[FEAT] Ruolo Consulente + Wizard Registrazione v2
- register.html: step 0 scelta profilo (Azienda / Consulente) - onboarding.html: wizard 4-step con P.IVA obbligatoria (auto-fetch CertiSource) - companies.html: nuova dashboard consulente con cards aziende e compliance score - common.js: org-switcher sidebar + role labels corretti per consulente - login.html: routing post-login (consulente → companies.html) - api.js: isConsultant(), setUserRole(), register con user_type - AuthController: user_type=consultant → role=consultant in users table - OnboardingController: multi-org per consulente, duplicate VAT check - 005_consultant_support.sql: aggiunge 'consultant' a user_organizations.role ENUM Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ba21534e6a
commit
7080695d06
@ -26,6 +26,8 @@ class AuthController extends BaseController
|
||||
$password = $this->getParam('password');
|
||||
$fullName = trim($this->getParam('full_name'));
|
||||
$phone = $this->getParam('phone');
|
||||
$userType = $this->getParam('user_type', 'azienda'); // 'azienda' | 'consultant'
|
||||
$role = ($userType === 'consultant') ? 'consultant' : 'employee';
|
||||
|
||||
// Validazione email
|
||||
if (!$this->validateEmail($email)) {
|
||||
@ -54,7 +56,7 @@ class AuthController extends BaseController
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'full_name' => $fullName,
|
||||
'phone' => $phone,
|
||||
'role' => 'employee',
|
||||
'role' => $role,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
@ -64,14 +66,14 @@ class AuthController extends BaseController
|
||||
|
||||
// Audit log
|
||||
$this->currentUser = ['id' => $userId];
|
||||
$this->logAudit('user_registered', 'user', $userId);
|
||||
$this->logAudit('user_registered', 'user', $userId, ['user_type' => $userType]);
|
||||
|
||||
$this->jsonSuccess([
|
||||
'user' => [
|
||||
'id' => $userId,
|
||||
'email' => $email,
|
||||
'full_name' => $fullName,
|
||||
'role' => 'employee',
|
||||
'role' => $role,
|
||||
],
|
||||
'access_token' => $accessToken,
|
||||
'refresh_token' => $refreshToken,
|
||||
|
||||
@ -132,17 +132,34 @@ class OnboardingController extends BaseController
|
||||
$this->validateRequired(['name', 'sector']);
|
||||
|
||||
$userId = $this->getCurrentUserId();
|
||||
$currentUser = $this->getCurrentUser();
|
||||
$isConsultant = ($currentUser['role'] === 'consultant');
|
||||
|
||||
// Check if user already has an organization
|
||||
// Check if user already has an organization (blocca solo per non-consulenti)
|
||||
$existingOrg = Database::fetchOne(
|
||||
'SELECT organization_id FROM user_organizations WHERE user_id = ? AND is_primary = 1',
|
||||
[$userId]
|
||||
);
|
||||
|
||||
if ($existingOrg) {
|
||||
if ($existingOrg && !$isConsultant) {
|
||||
$this->jsonError('Hai già un\'organizzazione configurata', 409, 'ORG_EXISTS');
|
||||
}
|
||||
|
||||
// Per consulenti: verifica che la P.IVA non sia già associata al loro account
|
||||
$vatNumber = $this->getParam('vat_number');
|
||||
if ($isConsultant && $vatNumber) {
|
||||
$cleanVat = preg_replace('/^IT/i', '', preg_replace('/\s+/', '', $vatNumber));
|
||||
$dupOrg = Database::fetchOne(
|
||||
'SELECT o.id FROM organizations o
|
||||
JOIN user_organizations uo ON uo.organization_id = o.id
|
||||
WHERE uo.user_id = ? AND o.vat_number = ?',
|
||||
[$userId, $cleanVat]
|
||||
);
|
||||
if ($dupOrg) {
|
||||
$this->jsonError('Questa azienda è già presente nel tuo portafoglio clienti', 409, 'ORG_DUPLICATE_VAT');
|
||||
}
|
||||
}
|
||||
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
// Create organization
|
||||
@ -178,12 +195,14 @@ class OnboardingController extends BaseController
|
||||
'voluntary_compliance' => $voluntaryCompliance,
|
||||
], 'id = ?', [$orgId]);
|
||||
|
||||
// Link user as org_admin
|
||||
// Link user: consulente → ruolo 'consultant', is_primary solo se prima org
|
||||
$orgRole = $isConsultant ? 'consultant' : 'org_admin';
|
||||
$isPrimary = $existingOrg ? 0 : 1;
|
||||
Database::insert('user_organizations', [
|
||||
'user_id' => $userId,
|
||||
'organization_id' => $orgId,
|
||||
'role' => 'org_admin',
|
||||
'is_primary' => 1,
|
||||
'role' => $orgRole,
|
||||
'is_primary' => $isPrimary,
|
||||
]);
|
||||
|
||||
// Update user profile if provided
|
||||
@ -207,7 +226,8 @@ class OnboardingController extends BaseController
|
||||
$this->logAudit('onboarding_completed', 'organization', $orgId, [
|
||||
'name' => $orgData['name'],
|
||||
'sector' => $orgData['sector'],
|
||||
'entity_type' => $entityType
|
||||
'entity_type' => $entityType,
|
||||
'is_consultant' => $isConsultant,
|
||||
]);
|
||||
|
||||
$this->jsonSuccess([
|
||||
|
||||
8
docs/sql/005_consultant_support.sql
Normal file
8
docs/sql/005_consultant_support.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- =============================================
|
||||
-- Migration 005: Supporto ruolo Consulente
|
||||
-- Aggiunge 'consultant' all'enum di user_organizations.role
|
||||
-- =============================================
|
||||
|
||||
ALTER TABLE user_organizations
|
||||
MODIFY COLUMN role ENUM('org_admin','compliance_manager','board_member','auditor','employee','consultant')
|
||||
NOT NULL DEFAULT 'employee';
|
||||
548
public/companies.html
Normal file
548
public/companies.html
Normal file
@ -0,0 +1,548 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Le Mie Aziende - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
.companies-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.companies-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.companies-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.search-box svg {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--gray-400);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
/* ── Company Cards Grid ─────────────────────────────────────── */
|
||||
.companies-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.company-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 24px;
|
||||
transition: all var(--transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.company-card:hover {
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: var(--primary-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.company-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.company-card-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.company-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.company-card-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.company-card-vat {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--gray-500);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.entity-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 99px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.entity-badge.essential {
|
||||
background: rgba(234, 67, 53, 0.1);
|
||||
color: #c5221f;
|
||||
}
|
||||
|
||||
.entity-badge.important {
|
||||
background: rgba(251, 188, 4, 0.15);
|
||||
color: #b06000;
|
||||
}
|
||||
|
||||
.entity-badge.not_applicable,
|
||||
.entity-badge.voluntary {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* ── Score Bar ───────────────────────────────────────────────── */
|
||||
.company-score-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.company-score-label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--gray-500);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.company-score-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--gray-100);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.company-score-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
.company-score-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ── Stats Row ───────────────────────────────────────────────── */
|
||||
.company-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.company-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.company-stat-value {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.company-stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* ── Card Footer ─────────────────────────────────────────────── */
|
||||
.company-card-footer {
|
||||
border-top: 1px solid var(--gray-100);
|
||||
padding-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.company-card-sector {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--gray-500);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── Empty State ─────────────────────────────────────────────── */
|
||||
.companies-empty {
|
||||
text-align: center;
|
||||
padding: 80px 24px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.companies-empty svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: var(--gray-300);
|
||||
margin: 0 auto 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.companies-empty h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.companies-empty p {
|
||||
font-size: 0.9375rem;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Loading skeleton ────────────────────────────────────────── */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--gray-100) 25%, var(--gray-50) 50%, var(--gray-100) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside id="sidebar" class="sidebar"></aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="page-container">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="companies-header">
|
||||
<div>
|
||||
<h1>Le Mie Aziende</h1>
|
||||
<p style="color:var(--gray-500);font-size:.9rem;margin-top:2px;">Gestisci il portafoglio clienti e monitora la compliance NIS2</p>
|
||||
</div>
|
||||
<div class="companies-header-actions">
|
||||
<div class="search-box">
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
|
||||
<input type="text" class="form-input" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)">
|
||||
</div>
|
||||
<a href="onboarding.html" class="btn btn-primary">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
||||
Aggiungi Cliente
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Companies Grid -->
|
||||
<div class="companies-grid" id="companies-grid">
|
||||
<!-- Skeleton loaders -->
|
||||
<div class="skeleton-card">
|
||||
<div style="display:flex;gap:12px;align-items:center">
|
||||
<div class="skeleton" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div>
|
||||
<div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div>
|
||||
</div>
|
||||
<div class="skeleton" style="height:6px"></div>
|
||||
<div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div>
|
||||
<div class="skeleton" style="height:36px"></div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div style="display:flex;gap:12px;align-items:center">
|
||||
<div class="skeleton" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div>
|
||||
<div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div>
|
||||
</div>
|
||||
<div class="skeleton" style="height:6px"></div>
|
||||
<div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div>
|
||||
<div class="skeleton" style="height:36px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state (hidden initially) -->
|
||||
<div class="companies-empty" id="companies-empty" style="display:none">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"/>
|
||||
</svg>
|
||||
<h3>Nessuna azienda ancora</h3>
|
||||
<p>Aggiungi il tuo primo cliente per iniziare a monitorare la compliance NIS2.</p>
|
||||
<a href="onboarding.html" class="btn btn-primary">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
||||
Aggiungi Primo Cliente
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
// Check that user is consultant, else redirect
|
||||
if (!api.isConsultant()) {
|
||||
window.location.href = 'dashboard.html';
|
||||
throw new Error('Not consultant');
|
||||
}
|
||||
|
||||
loadSidebar();
|
||||
|
||||
// ── Data ────────────────────────────────────────────────────────
|
||||
let allCompanies = [];
|
||||
|
||||
// ── Load companies ───────────────────────────────────────────────
|
||||
async function loadCompanies() {
|
||||
const result = await api.listOrganizations();
|
||||
const grid = document.getElementById('companies-grid');
|
||||
const emptyState = document.getElementById('companies-empty');
|
||||
|
||||
if (!result.success) {
|
||||
showNotification('Errore nel caricamento delle aziende.', 'error');
|
||||
grid.innerHTML = '';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const orgs = result.data || [];
|
||||
allCompanies = orgs;
|
||||
|
||||
if (orgs.length === 0) {
|
||||
grid.innerHTML = '';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
renderCompanies(orgs);
|
||||
|
||||
// Load compliance scores in background
|
||||
orgs.forEach(org => loadOrgScore(org.id || org.organization_id));
|
||||
}
|
||||
|
||||
// ── Render companies ─────────────────────────────────────────────
|
||||
function renderCompanies(orgs) {
|
||||
const grid = document.getElementById('companies-grid');
|
||||
grid.innerHTML = orgs.map(org => renderCompanyCard(org)).join('');
|
||||
}
|
||||
|
||||
function renderCompanyCard(org) {
|
||||
const orgId = org.id || org.organization_id;
|
||||
const name = org.name || 'Azienda';
|
||||
const vat = org.vat_number ? 'IT' + org.vat_number : '-';
|
||||
const sector = org.sector || '';
|
||||
const entityType = org.entity_type || 'not_applicable';
|
||||
const voluntary = org.voluntary_compliance ? true : false;
|
||||
|
||||
const initials = name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
|
||||
|
||||
const entityBadge = renderEntityBadge(entityType, voluntary);
|
||||
const sectorLabel = getSectorLabel(sector);
|
||||
|
||||
return `
|
||||
<div class="company-card" data-name="${escapeHtml(name.toLowerCase())}" data-org-id="${orgId}">
|
||||
<div class="company-card-header">
|
||||
<div class="company-card-avatar">${escapeHtml(initials)}</div>
|
||||
<div class="company-card-info">
|
||||
<div class="company-card-name" title="${escapeHtml(name)}">${escapeHtml(name)}</div>
|
||||
<div class="company-card-vat">${escapeHtml(vat)}</div>
|
||||
</div>
|
||||
${entityBadge}
|
||||
</div>
|
||||
|
||||
<!-- Score bar (loaded async) -->
|
||||
<div class="company-score-row" id="score-row-${orgId}" style="display:none">
|
||||
<span class="company-score-label">Compliance</span>
|
||||
<div class="company-score-bar">
|
||||
<div class="company-score-bar-fill" id="score-fill-${orgId}" style="width:0%;background:#ccc"></div>
|
||||
</div>
|
||||
<span class="company-score-value" id="score-val-${orgId}">-</span>
|
||||
</div>
|
||||
|
||||
<!-- Stats row (loaded async) -->
|
||||
<div class="company-stats" id="stats-row-${orgId}" style="display:none">
|
||||
<div class="company-stat">
|
||||
<span class="company-stat-value" id="stat-risks-${orgId}">-</span>
|
||||
<span class="company-stat-label">Rischi aperti</span>
|
||||
</div>
|
||||
<div class="company-stat">
|
||||
<span class="company-stat-value" id="stat-incidents-${orgId}">-</span>
|
||||
<span class="company-stat-label">Incidenti attivi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="company-card-footer">
|
||||
<span class="company-card-sector">${escapeHtml(sectorLabel)}</span>
|
||||
<button class="btn btn-primary btn-sm" onclick="manageCompany(${orgId})">
|
||||
Gestisci
|
||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderEntityBadge(type, voluntary) {
|
||||
if (voluntary) {
|
||||
return `<span class="entity-badge voluntary">Volontaria</span>`;
|
||||
}
|
||||
if (type === 'essential') {
|
||||
return `<span class="entity-badge essential">Essenziale</span>`;
|
||||
}
|
||||
if (type === 'important') {
|
||||
return `<span class="entity-badge important">Importante</span>`;
|
||||
}
|
||||
return `<span class="entity-badge not_applicable">Non NIS2</span>`;
|
||||
}
|
||||
|
||||
// ── Load compliance score per org ────────────────────────────────
|
||||
async function loadOrgScore(orgId) {
|
||||
try {
|
||||
// Temporarily switch to this org for the API call
|
||||
const savedOrgId = api.orgId;
|
||||
api.orgId = orgId;
|
||||
const result = await api.getDashboardOverview();
|
||||
api.orgId = savedOrgId;
|
||||
|
||||
if (result.success && result.data) {
|
||||
const score = result.data.compliance_score || 0;
|
||||
const risks = result.data.open_risks || 0;
|
||||
const incidents = result.data.active_incidents || 0;
|
||||
|
||||
const scoreRow = document.getElementById('score-row-' + orgId);
|
||||
const scoreVal = document.getElementById('score-val-' + orgId);
|
||||
const scoreFill = document.getElementById('score-fill-' + orgId);
|
||||
const statsRow = document.getElementById('stats-row-' + orgId);
|
||||
const statRisks = document.getElementById('stat-risks-' + orgId);
|
||||
const statIncidents = document.getElementById('stat-incidents-' + orgId);
|
||||
|
||||
if (scoreRow) scoreRow.style.display = 'flex';
|
||||
if (scoreVal) {
|
||||
scoreVal.textContent = score + '%';
|
||||
scoreVal.style.color = getScoreColor(score);
|
||||
}
|
||||
if (scoreFill) {
|
||||
scoreFill.style.width = score + '%';
|
||||
scoreFill.style.background = getScoreColor(score);
|
||||
}
|
||||
if (statsRow) statsRow.style.display = 'flex';
|
||||
if (statRisks) statRisks.textContent = risks;
|
||||
if (statIncidents) statIncidents.textContent = incidents;
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently ignore per-org errors
|
||||
}
|
||||
}
|
||||
|
||||
// ── Manage company (switch org + go to dashboard) ─────────────────
|
||||
function manageCompany(orgId) {
|
||||
api.setOrganization(orgId);
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
// ── Search / Filter ──────────────────────────────────────────────
|
||||
function filterCompanies(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
const cards = document.querySelectorAll('.company-card');
|
||||
let anyVisible = false;
|
||||
|
||||
cards.forEach(card => {
|
||||
const name = card.dataset.name || '';
|
||||
const visible = !q || name.includes(q);
|
||||
card.style.display = visible ? '' : 'none';
|
||||
if (visible) anyVisible = true;
|
||||
});
|
||||
|
||||
const emptyState = document.getElementById('companies-empty');
|
||||
if (emptyState) {
|
||||
emptyState.style.display = (!anyVisible && allCompanies.length > 0) ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sector labels ────────────────────────────────────────────────
|
||||
function getSectorLabel(sector) {
|
||||
const labels = {
|
||||
energy_electricity: 'Energia - Elettricità',
|
||||
energy_oil: 'Energia - Petrolio', energy_gas: 'Energia - Gas',
|
||||
energy_hydrogen: 'Energia - Idrogeno',
|
||||
transport_air: 'Trasporti - Aereo', transport_rail: 'Trasporti - Ferroviario',
|
||||
transport_water: 'Trasporti - Marittimo', transport_road: 'Trasporti - Stradale',
|
||||
banking: 'Banche', financial_markets: 'Mercati Finanziari',
|
||||
health: 'Sanità', drinking_water: 'Acqua Potabile',
|
||||
waste_water: 'Acque Reflue', digital_infrastructure: 'Infrastruttura Digitale',
|
||||
ict_service_management: 'Gestione ICT B2B', public_administration: 'PA',
|
||||
space: 'Spazio', postal_courier: 'Postale/Corrieri',
|
||||
waste_management: 'Gestione Rifiuti', chemicals: 'Chimica',
|
||||
food: 'Alimentare', manufacturing_medical: 'Dispositivi Medici',
|
||||
manufacturing_computers: 'Computer/Elettronica',
|
||||
manufacturing_electrical: 'App. Elettriche',
|
||||
manufacturing_machinery: 'Macchinari', manufacturing_vehicles: 'Autoveicoli',
|
||||
manufacturing_transport: 'Mezzi di Trasporto',
|
||||
digital_providers: 'Fornitori Digitali', research: 'Ricerca', other: 'Altro'
|
||||
};
|
||||
return labels[sector] || sector || 'Settore non specificato';
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────────
|
||||
loadCompanies();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -72,6 +72,7 @@ class NIS2API {
|
||||
const result = await this.post('/auth/login', { email, password });
|
||||
if (result.success) {
|
||||
this.setTokens(result.data.access_token, result.data.refresh_token);
|
||||
this.setUserRole(result.data.user.role);
|
||||
if (result.data.organizations && result.data.organizations.length > 0) {
|
||||
const primary = result.data.organizations.find(o => o.is_primary) || result.data.organizations[0];
|
||||
this.setOrganization(primary.organization_id);
|
||||
@ -80,10 +81,11 @@ class NIS2API {
|
||||
return result;
|
||||
}
|
||||
|
||||
async register(email, password, fullName) {
|
||||
const result = await this.post('/auth/register', { email, password, full_name: fullName });
|
||||
async register(email, password, fullName, userType = 'azienda') {
|
||||
const result = await this.post('/auth/register', { email, password, full_name: fullName, user_type: userType });
|
||||
if (result.success) {
|
||||
this.setTokens(result.data.access_token, result.data.refresh_token);
|
||||
localStorage.setItem('nis2_user_role', result.data.user.role);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -122,6 +124,20 @@ class NIS2API {
|
||||
localStorage.removeItem('nis2_access_token');
|
||||
localStorage.removeItem('nis2_refresh_token');
|
||||
localStorage.removeItem('nis2_org_id');
|
||||
localStorage.removeItem('nis2_user_role');
|
||||
}
|
||||
|
||||
// Salva ruolo utente al login
|
||||
setUserRole(role) {
|
||||
localStorage.setItem('nis2_user_role', role);
|
||||
}
|
||||
|
||||
getUserRole() {
|
||||
return localStorage.getItem('nis2_user_role');
|
||||
}
|
||||
|
||||
isConsultant() {
|
||||
return this.getUserRole() === 'consultant';
|
||||
}
|
||||
|
||||
setOrganization(orgId) {
|
||||
|
||||
@ -230,6 +230,9 @@ function loadSidebar() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Org-switcher area (populated async for consultants)
|
||||
navHTML += `<div id="sidebar-org-switcher" style="display:none;padding:8px 12px;border-bottom:1px solid var(--gray-100);"></div>`;
|
||||
|
||||
// Nav sections
|
||||
navHTML += '<nav class="sidebar-nav">';
|
||||
for (const section of navItems) {
|
||||
@ -263,13 +266,23 @@ function loadSidebar() {
|
||||
|
||||
container.innerHTML = navHTML;
|
||||
|
||||
// Carica info utente
|
||||
// Carica info utente (e org-switcher se consulente)
|
||||
_loadUserInfo();
|
||||
|
||||
// Mobile toggle
|
||||
_setupMobileToggle();
|
||||
}
|
||||
|
||||
const _roleLabels = {
|
||||
super_admin: 'Super Admin',
|
||||
org_admin: 'Admin Org',
|
||||
compliance_manager: 'Compliance Manager',
|
||||
board_member: 'Membro CDA',
|
||||
auditor: 'Auditor',
|
||||
employee: 'Utente',
|
||||
consultant: 'Consulente',
|
||||
};
|
||||
|
||||
async function _loadUserInfo() {
|
||||
try {
|
||||
const result = await api.getMe();
|
||||
@ -288,13 +301,100 @@ async function _loadUserInfo() {
|
||||
.toUpperCase();
|
||||
avatarEl.textContent = initials;
|
||||
}
|
||||
if (roleEl) roleEl.textContent = user.role === 'admin' ? 'Amministratore' : 'Utente';
|
||||
if (roleEl) roleEl.textContent = _roleLabels[user.role] || user.role || 'Utente';
|
||||
|
||||
// Save role to localStorage (ensures isConsultant() works across pages)
|
||||
if (user.role) api.setUserRole(user.role);
|
||||
|
||||
// For consultants: render org-switcher
|
||||
if (user.role === 'consultant') {
|
||||
_loadConsultantOrgSwitcher();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silenzioso
|
||||
}
|
||||
}
|
||||
|
||||
async function _loadConsultantOrgSwitcher() {
|
||||
try {
|
||||
const result = await api.listOrganizations();
|
||||
if (!result.success || !result.data || result.data.length === 0) return;
|
||||
|
||||
const orgs = result.data;
|
||||
const currentOrgId = parseInt(api.orgId);
|
||||
const currentOrg = orgs.find(o => (o.id || o.organization_id) === currentOrgId);
|
||||
|
||||
const switcher = document.getElementById('sidebar-org-switcher');
|
||||
if (!switcher) return;
|
||||
|
||||
const dropdownId = 'org-dropdown-' + Date.now();
|
||||
switcher.style.display = 'block';
|
||||
switcher.innerHTML = `
|
||||
<div style="position:relative">
|
||||
<button onclick="document.getElementById('${dropdownId}').classList.toggle('open')"
|
||||
style="width:100%;display:flex;align-items:center;gap:8px;padding:6px 10px;
|
||||
background:var(--gray-50);border:1px solid var(--gray-200);border-radius:6px;
|
||||
cursor:pointer;font-size:.8rem;font-weight:600;color:var(--gray-700);">
|
||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="flex-shrink:0;color:var(--primary)">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span style="flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
${currentOrg ? currentOrg.name : 'Seleziona azienda'}
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor" style="flex-shrink:0">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="${dropdownId}" class="org-switcher-dropdown"
|
||||
style="display:none;position:absolute;left:0;right:0;top:calc(100% + 4px);
|
||||
background:var(--card-bg);border:1px solid var(--gray-200);border-radius:6px;
|
||||
box-shadow:var(--card-shadow);z-index:200;overflow:hidden;">
|
||||
${orgs.map(org => {
|
||||
const orgId = org.id || org.organization_id;
|
||||
const isActive = orgId === currentOrgId;
|
||||
return `<button onclick="_switchOrg(${orgId})"
|
||||
style="width:100%;text-align:left;padding:8px 12px;
|
||||
background:${isActive ? 'var(--primary-bg)' : 'transparent'};
|
||||
border:none;cursor:pointer;font-size:.8rem;
|
||||
color:${isActive ? 'var(--primary)' : 'var(--gray-700)'};
|
||||
font-weight:${isActive ? '600' : '400'};">
|
||||
${isActive ? '✓ ' : ''}${org.name}
|
||||
</button>`;
|
||||
}).join('')}
|
||||
<div style="border-top:1px solid var(--gray-100)">
|
||||
<a href="companies.html"
|
||||
style="display:block;padding:8px 12px;font-size:.8rem;
|
||||
color:var(--primary);text-decoration:none;">
|
||||
← Tutte le aziende
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
const dd = document.getElementById(dropdownId);
|
||||
if (dd && !switcher.contains(e.target)) dd.style.display = 'none';
|
||||
});
|
||||
// Override toggle to handle display
|
||||
const ddEl = document.getElementById(dropdownId);
|
||||
if (ddEl) {
|
||||
const btn = switcher.querySelector('button');
|
||||
if (btn) btn.onclick = () => {
|
||||
ddEl.style.display = ddEl.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Silenzioso
|
||||
}
|
||||
}
|
||||
|
||||
function _switchOrg(orgId) {
|
||||
api.setOrganization(orgId);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function _setupMobileToggle() {
|
||||
// Crea pulsante toggle se non esiste
|
||||
if (!document.querySelector('.sidebar-toggle')) {
|
||||
|
||||
@ -82,11 +82,14 @@
|
||||
const result = await api.login(email, password);
|
||||
|
||||
if (result.success) {
|
||||
// Controlla se l'utente ha un'organizzazione
|
||||
if (result.data.organizations && result.data.organizations.length > 0) {
|
||||
window.location.href = 'dashboard.html';
|
||||
} else {
|
||||
const isConsultant = result.data.user && result.data.user.role === 'consultant';
|
||||
const hasOrgs = result.data.organizations && result.data.organizations.length > 0;
|
||||
if (!hasOrgs) {
|
||||
window.location.href = 'onboarding.html';
|
||||
} else if (isConsultant) {
|
||||
window.location.href = 'companies.html';
|
||||
} else {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
} else {
|
||||
errorEl.textContent = result.message || 'Credenziali non valide.';
|
||||
|
||||
@ -745,26 +745,21 @@
|
||||
<div class="stepper" id="stepper">
|
||||
<div class="stepper-step active" data-step="1">
|
||||
<div class="stepper-number">1</div>
|
||||
<span class="stepper-label">Benvenuto</span>
|
||||
<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">Acquisizione Dati</span>
|
||||
<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">Dati Aziendali</span>
|
||||
<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">Il Tuo Profilo</span>
|
||||
</div>
|
||||
<div class="stepper-connector" data-after="4"></div>
|
||||
<div class="stepper-step" data-step="5">
|
||||
<div class="stepper-number">5</div>
|
||||
<span class="stepper-label">Classificazione</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -774,66 +769,48 @@
|
||||
<div class="wizard-body">
|
||||
<div class="wizard-content">
|
||||
|
||||
<!-- ═══ STEP 1: Benvenuto ═══ -->
|
||||
<!-- ═══ STEP 1: Partita IVA ═══ -->
|
||||
<div class="wizard-step active" id="step-1">
|
||||
<h2 class="wizard-step-title">Configura il tuo ambiente NIS2</h2>
|
||||
<p class="wizard-step-subtitle">Per iniziare, abbiamo bisogno dei dati della tua azienda. Puoi fornirli in tre modi:</p>
|
||||
<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="option-cards">
|
||||
<!-- Carica Visura -->
|
||||
<div class="option-card" data-method="visura" onclick="selectMethod('visura')">
|
||||
<div class="option-card-check">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><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>
|
||||
<div class="card" style="max-width:520px; margin:0 auto;">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="s1-vat">Partita IVA <span class="required">*</span></label>
|
||||
<div style="display:flex; gap:.5rem;">
|
||||
<span style="display:flex;align-items:center;padding:0 12px;background:var(--gray-100);border:1px solid var(--gray-300);border-radius:var(--border-radius);font-weight:600;color:var(--gray-600);">IT</span>
|
||||
<input type="text" id="s1-vat" class="form-input" placeholder="12345678901" maxlength="11"
|
||||
style="flex:1;" oninput="this.value=this.value.replace(/\D/g,'')">
|
||||
</div>
|
||||
<div class="form-help">11 cifre numeriche senza prefisso IT</div>
|
||||
</div>
|
||||
<div class="option-card-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="12" y1="18" x2="12" y2="12"/>
|
||||
<line x1="9" y1="15" x2="15" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-card-title">Carica Visura Camerale</div>
|
||||
<div class="option-card-desc">Carica il PDF della visura camerale e i dati verranno estratti automaticamente con l'AI</div>
|
||||
</div>
|
||||
|
||||
<!-- CertiSource -->
|
||||
<div class="option-card" data-method="certisource" onclick="selectMethod('certisource')">
|
||||
<div class="option-card-check">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><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>
|
||||
</div>
|
||||
<div class="option-card-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
<path d="M11 8v6"/>
|
||||
<path d="M8 11h6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-card-title">Recupera con CertiSource</div>
|
||||
<div class="option-card-desc">Inserisci la Partita IVA e recupereremo automaticamente i dati aziendali tramite CertiSource</div>
|
||||
</div>
|
||||
<div id="s1-result" style="display:none; padding:.75rem; border-radius:8px; margin-bottom:1rem;"></div>
|
||||
|
||||
<!-- Manuale -->
|
||||
<div class="option-card" data-method="manual" onclick="selectMethod('manual')">
|
||||
<div class="option-card-check">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><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>
|
||||
</div>
|
||||
<div class="option-card-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="option-card-title">Inserisci Manualmente</div>
|
||||
<div class="option-card-desc">Compila tu stesso i dati dell'azienda</div>
|
||||
<button class="btn btn-primary btn-lg w-full" id="s1-fetch-btn" onclick="step1FetchCompany()">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
|
||||
Cerca Azienda
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions" id="step1-actions" style="display:none;">
|
||||
<div class="wizard-loading" id="s1-loading">
|
||||
<div class="spinner spinner-lg"></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()" style="color:var(--gray-500);font-size:.85rem;">
|
||||
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 btn-lg" onclick="goToStep(2)">
|
||||
<button class="btn btn-primary btn-lg" id="s1-continue-btn" onclick="goToStep(2)" style="display:none;">
|
||||
Continua
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</button>
|
||||
@ -841,100 +818,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ STEP 2: Acquisizione Dati ═══ -->
|
||||
<div class="wizard-step" id="step-2">
|
||||
|
||||
<!-- 2a: Visura Upload -->
|
||||
<div id="step2-visura" style="display:none;">
|
||||
<h2 class="wizard-step-title">Carica la Visura Camerale</h2>
|
||||
<p class="wizard-step-subtitle">Carica il file PDF della visura camerale. I dati aziendali verranno estratti automaticamente tramite intelligenza artificiale.</p>
|
||||
|
||||
<div class="upload-area" id="upload-area">
|
||||
<input type="file" id="visura-file" accept=".pdf" />
|
||||
<div class="upload-area-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="upload-area-title">Trascina qui il file PDF oppure clicca per selezionarlo</div>
|
||||
<div class="upload-area-hint">Rilascia il file per caricarlo</div>
|
||||
<div class="upload-area-formats">Formato accettato: PDF - Dimensione massima: 10 MB</div>
|
||||
<div class="upload-file-info" id="upload-file-info">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
|
||||
<span id="upload-file-name"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px; text-align:center;" id="visura-upload-btn-area">
|
||||
<button class="btn btn-primary btn-lg" id="analyze-visura-btn" disabled onclick="analyzeVisura()">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
||||
Analizza Visura
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="wizard-loading" id="visura-loading">
|
||||
<div class="spinner spinner-lg"></div>
|
||||
<div class="wizard-loading-text">Analisi AI in corso...</div>
|
||||
<div class="wizard-loading-subtext">Stiamo estraendo i dati dalla visura camerale</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(1)">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
|
||||
Indietro
|
||||
</button>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2b: CertiSource -->
|
||||
<div id="step2-certisource" style="display:none;">
|
||||
<h2 class="wizard-step-title">Recupera Dati con CertiSource</h2>
|
||||
<p class="wizard-step-subtitle">Inserisci la Partita IVA dell'azienda e recupereremo automaticamente i dati aziendali dal registro delle imprese.</p>
|
||||
|
||||
<div class="card" style="max-width:520px; margin:0 auto;">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cs-vat">Partita IVA <span class="required">*</span></label>
|
||||
<input type="text" id="cs-vat" class="form-input" placeholder="Es. 12345678901 o IT12345678901" maxlength="16">
|
||||
<div class="form-help">Inserisci la Partita IVA con o senza prefisso paese</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-lg w-full" id="fetch-company-btn" onclick="fetchCompany()">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
|
||||
Recupera Dati
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="wizard-loading" id="certisource-loading">
|
||||
<div class="spinner spinner-lg"></div>
|
||||
<div class="wizard-loading-text">Recupero dati da CertiSource...</div>
|
||||
<div class="wizard-loading-subtext">Stiamo interrogando il registro delle imprese</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center; margin-top:24px;">
|
||||
<div class="certisource-badge">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
|
||||
Powered by CertiSource
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(1)">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
|
||||
Indietro
|
||||
</button>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ STEP 3: Dati Aziendali ═══ -->
|
||||
<div class="wizard-step" id="step-3">
|
||||
<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>
|
||||
|
||||
@ -1044,7 +929,7 @@
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(wizardState.method === 'manual' ? 1 : 2)">
|
||||
<button class="btn btn-secondary" onclick="goToStep(1)">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
|
||||
Indietro
|
||||
</button>
|
||||
@ -1058,7 +943,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ═══ STEP 4: Il Tuo Profilo ═══ -->
|
||||
<div class="wizard-step" id="step-4">
|
||||
<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>
|
||||
|
||||
@ -1096,7 +981,7 @@
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(3)">
|
||||
<button class="btn btn-secondary" onclick="goToStep(2)">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
|
||||
Indietro
|
||||
</button>
|
||||
@ -1110,7 +995,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ═══ STEP 5: Classificazione NIS2 e Riepilogo ═══ -->
|
||||
<div class="wizard-step" id="step-5">
|
||||
<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>
|
||||
|
||||
@ -1175,7 +1060,7 @@
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(4)">
|
||||
<button class="btn btn-secondary" onclick="goToStep(3)">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
|
||||
Indietro
|
||||
</button>
|
||||
@ -1259,35 +1144,78 @@
|
||||
return parseInt(str) || '';
|
||||
}
|
||||
|
||||
// ── Step 1: Method selection ────────────────────────────────────
|
||||
function selectMethod(method) {
|
||||
wizardState.method = method;
|
||||
// ── 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;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll('.option-card').forEach(card => {
|
||||
card.classList.toggle('selected', card.dataset.method === method);
|
||||
});
|
||||
const btn = document.getElementById('s1-fetch-btn');
|
||||
const loading = document.getElementById('s1-loading');
|
||||
const resultDiv = document.getElementById('s1-result');
|
||||
|
||||
// Show continue button
|
||||
document.getElementById('step1-actions').style.display = 'flex';
|
||||
btn.disabled = true;
|
||||
loading.classList.add('active');
|
||||
resultDiv.style.display = '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.style.cssText = 'display:block; padding:.75rem; border-radius:8px; background:var(--success-bg); color:var(--secondary);';
|
||||
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.style.cssText = 'display:block; padding:.75rem; border-radius:8px; background:var(--warning-bg,#fff3cd); color:#856404;';
|
||||
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) {
|
||||
const prevStep = wizardState.currentStep;
|
||||
|
||||
// If going from step 1 to step 2, handle method routing
|
||||
if (prevStep === 1 && step === 2) {
|
||||
if (!wizardState.method) {
|
||||
showNotification('Seleziona un metodo per procedere.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (wizardState.method === 'manual') {
|
||||
// Skip step 2, go directly to step 3
|
||||
step = 3;
|
||||
}
|
||||
}
|
||||
|
||||
wizardState.currentStep = step;
|
||||
|
||||
// Hide all steps
|
||||
@ -1297,24 +1225,18 @@
|
||||
const targetStep = document.getElementById('step-' + step);
|
||||
if (targetStep) targetStep.classList.add('active');
|
||||
|
||||
// If step 2, show the appropriate sub-panel
|
||||
// Step 2: populate company form from wizard state
|
||||
if (step === 2) {
|
||||
document.getElementById('step2-visura').style.display = wizardState.method === 'visura' ? 'block' : 'none';
|
||||
document.getElementById('step2-certisource').style.display = wizardState.method === 'certisource' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// If step 3, populate form from state
|
||||
if (step === 3) {
|
||||
populateCompanyForm();
|
||||
}
|
||||
|
||||
// If step 4, load user profile
|
||||
if (step === 4) {
|
||||
// Step 3: load user profile
|
||||
if (step === 3) {
|
||||
loadUserProfile();
|
||||
}
|
||||
|
||||
// If step 5, run classification and populate summary
|
||||
if (step === 5) {
|
||||
// Step 4: run classification and populate summary
|
||||
if (step === 4) {
|
||||
runClassificationAndSummary();
|
||||
}
|
||||
|
||||
@ -1349,34 +1271,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Step 2a: Visura Upload ──────────────────────────────────────
|
||||
const uploadArea = document.getElementById('upload-area');
|
||||
const visuraFileInput = document.getElementById('visura-file');
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleVisuraFile(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
visuraFileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleVisuraFile(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
// ── Step 2a: Visura Upload (legacy, kept for API completeness) ──
|
||||
|
||||
function handleVisuraFile(file) {
|
||||
// Validate type
|
||||
@ -1509,7 +1404,7 @@
|
||||
showNotification('Dati aziendali recuperati con successo!', 'success');
|
||||
|
||||
// Auto-advance to step 3
|
||||
setTimeout(() => goToStep(3), 600);
|
||||
setTimeout(() => goToStep(2), 600);
|
||||
} else {
|
||||
showNotification(result.message || 'Impossibile recuperare i dati. Verifica la Partita IVA.', 'error');
|
||||
}
|
||||
@ -1556,7 +1451,7 @@
|
||||
|
||||
// Apply auto-fill visual indicators
|
||||
const autoFields = wizardState.autoFilledFields;
|
||||
document.querySelectorAll('#step-3 .form-input, #step-3 .form-select').forEach(el => {
|
||||
document.querySelectorAll('#step-2 .form-input, #step-2 .form-select').forEach(el => {
|
||||
const wrapper = el.closest('.form-group');
|
||||
if (!wrapper) return;
|
||||
|
||||
@ -1569,7 +1464,7 @@
|
||||
wrapper.classList.add('auto-filled');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'auto-fill-badge';
|
||||
badge.textContent = 'da visura';
|
||||
badge.textContent = wizardState.dataSource ? 'auto-compilato' : 'da visura';
|
||||
const label = wrapper.querySelector('.form-label');
|
||||
if (label) label.appendChild(badge);
|
||||
}
|
||||
@ -1618,7 +1513,7 @@
|
||||
wizardState.company.employee_count = parseInt(employees);
|
||||
wizardState.company.annual_turnover = parseInt(turnover);
|
||||
|
||||
goToStep(4);
|
||||
goToStep(3);
|
||||
}
|
||||
|
||||
// ── Step 4: User Profile ────────────────────────────────────────
|
||||
@ -1650,7 +1545,7 @@
|
||||
wizardState.profile.role = role;
|
||||
wizardState.profile.phone = document.getElementById('user-phone').value.trim();
|
||||
|
||||
goToStep(5);
|
||||
goToStep(4);
|
||||
}
|
||||
|
||||
// ── Step 5: Classification & Summary ────────────────────────────
|
||||
@ -1835,7 +1730,8 @@
|
||||
showNotification('Configurazione completata con successo!', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = 'dashboard.html';
|
||||
const dest = api.isConsultant() ? 'companies.html' : 'dashboard.html';
|
||||
window.location.href = dest;
|
||||
}, 1000);
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nel completamento.', 'error');
|
||||
@ -1849,11 +1745,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Allow CertiSource VAT input to submit with Enter ────────────
|
||||
document.getElementById('cs-vat').addEventListener('keypress', (e) => {
|
||||
// ── Allow P.IVA input to submit with Enter ───────────────────────
|
||||
document.getElementById('s1-vat').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
fetchCompany();
|
||||
step1FetchCompany();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -5,6 +5,28 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registrazione - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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-0 { display: block; }
|
||||
#step-1 { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-page">
|
||||
@ -19,50 +41,82 @@
|
||||
</div>
|
||||
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
|
||||
</div>
|
||||
<p class="auth-subtitle">Crea il tuo account</p>
|
||||
<p class="auth-subtitle" id="auth-subtitle">Scegli il tuo profilo</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>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="register-error"></div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Indirizzo Email <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email" class="form-input"
|
||||
placeholder="nome@azienda.it" autocomplete="email" required>
|
||||
</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="password-strength" id="password-strength">
|
||||
<div class="password-strength-bar">
|
||||
<div class="password-strength-segment" id="ps-1"></div>
|
||||
<div class="password-strength-segment" id="ps-2"></div>
|
||||
<div class="password-strength-segment" id="ps-3"></div>
|
||||
<div class="password-strength-segment" id="ps-4"></div>
|
||||
</div>
|
||||
<div class="password-strength-text" id="ps-text"></div>
|
||||
<!-- STEP 0: Scelta profilo -->
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-full" id="register-btn">
|
||||
Crea Account
|
||||
<button class="btn btn-primary btn-lg w-full" id="btn-next" onclick="goToStep1()" disabled>
|
||||
Continua →
|
||||
</button>
|
||||
</form>
|
||||
</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>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Indirizzo Email <span class="required">*</span></label>
|
||||
<input type="email" id="email" name="email" class="form-input"
|
||||
placeholder="nome@azienda.it" autocomplete="email" required>
|
||||
</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="password-strength" id="password-strength">
|
||||
<div class="password-strength-bar">
|
||||
<div class="password-strength-segment" id="ps-1"></div>
|
||||
<div class="password-strength-segment" id="ps-2"></div>
|
||||
<div class="password-strength-segment" id="ps-3"></div>
|
||||
<div class="password-strength-segment" id="ps-4"></div>
|
||||
</div>
|
||||
<div class="password-strength-text" id="ps-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div style="display:flex; gap:.75rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="goToStep0()" style="flex:0 0 auto;">
|
||||
← Indietro
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="register-btn" style="flex:1;">
|
||||
Crea Account
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
@ -74,56 +128,68 @@
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script>
|
||||
// Se gia' autenticato, vai alla dashboard
|
||||
if (api.isAuthenticated()) {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
|
||||
const form = document.getElementById('register-form');
|
||||
const errorEl = document.getElementById('register-error');
|
||||
const registerBtn = document.getElementById('register-btn');
|
||||
const passwordInput = document.getElementById('password');
|
||||
let selectedUserType = null;
|
||||
|
||||
// ── Password Strength Indicator ──────────────────────────
|
||||
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;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// Password Strength
|
||||
const passwordInput = document.getElementById('password');
|
||||
passwordInput.addEventListener('input', () => {
|
||||
const val = passwordInput.value;
|
||||
const strength = calcPasswordStrength(val);
|
||||
updateStrengthUI(strength);
|
||||
updateStrengthUI(calcPasswordStrength(passwordInput.value));
|
||||
});
|
||||
|
||||
function calcPasswordStrength(password) {
|
||||
let score = 0;
|
||||
if (password.length >= 8) score++;
|
||||
if (password.length >= 12) score++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
|
||||
if (/\d/.test(password)) score++;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
||||
// Normalize to 0-4
|
||||
if (score <= 1) return 1;
|
||||
if (score === 2) return 2;
|
||||
if (score === 3) return 3;
|
||||
return 4;
|
||||
function calcPasswordStrength(pw) {
|
||||
let s = 0;
|
||||
if (pw.length >= 8) s++;
|
||||
if (pw.length >= 12) s++;
|
||||
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++;
|
||||
if (/\d/.test(pw)) s++;
|
||||
if (/[^a-zA-Z0-9]/.test(pw)) s++;
|
||||
return Math.min(4, Math.max(1, s <= 1 ? 1 : s === 2 ? 2 : s === 3 ? 3 : 4));
|
||||
}
|
||||
|
||||
function updateStrengthUI(level) {
|
||||
const labels = { 1: 'Debole', 2: 'Sufficiente', 3: 'Buona', 4: 'Forte' };
|
||||
const classes = { 1: 'weak', 2: 'fair', 3: 'good', 4: 'strong' };
|
||||
const textEl = document.getElementById('ps-text');
|
||||
|
||||
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 && passwordInput.value.length > 0) seg.classList.add('active', classes[level]);
|
||||
}
|
||||
|
||||
textEl.textContent = passwordInput.value.length > 0 ? labels[level] : '';
|
||||
document.getElementById('ps-text').textContent = passwordInput.value.length > 0 ? labels[level] : '';
|
||||
}
|
||||
|
||||
// ── Form Submit ──────────────────────────────────────────
|
||||
form.addEventListener('submit', async (e) => {
|
||||
// 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();
|
||||
@ -131,37 +197,31 @@
|
||||
const password = document.getElementById('password').value;
|
||||
const passwordConfirm = document.getElementById('password-confirm').value;
|
||||
|
||||
// Validazione
|
||||
if (!fullname || !email || !password || !passwordConfirm) {
|
||||
errorEl.textContent = 'Tutti i campi sono obbligatori.';
|
||||
errorEl.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
errorEl.textContent = 'La password deve avere almeno 8 caratteri.';
|
||||
errorEl.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
errorEl.textContent = 'Le password non coincidono.';
|
||||
errorEl.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
registerBtn.disabled = true;
|
||||
registerBtn.textContent = 'Registrazione in corso...';
|
||||
const btn = document.getElementById('register-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Registrazione in corso...';
|
||||
|
||||
try {
|
||||
const result = await api.register(email, password, fullname);
|
||||
|
||||
const result = await api.register(email, password, fullname, selectedUserType);
|
||||
if (result.success) {
|
||||
showNotification('Account creato con successo!', 'success');
|
||||
// Dopo la registrazione, porta al setup organizzazione
|
||||
setTimeout(() => {
|
||||
window.location.href = 'onboarding.html';
|
||||
}, 500);
|
||||
setTimeout(() => { window.location.href = 'onboarding.html'; }, 500);
|
||||
} else {
|
||||
errorEl.textContent = result.message || 'Errore durante la registrazione.';
|
||||
errorEl.classList.add('visible');
|
||||
@ -170,8 +230,8 @@
|
||||
errorEl.textContent = 'Errore di connessione al server.';
|
||||
errorEl.classList.add('visible');
|
||||
} finally {
|
||||
registerBtn.disabled = false;
|
||||
registerBtn.textContent = 'Crea Account';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Crea Account';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user