From 9aa2788c68022c2ae57fa6361a640c540acdafb1 Mon Sep 17 00:00:00 2001 From: Cristiano Benassati Date: Tue, 17 Feb 2026 19:01:34 +0100 Subject: [PATCH] [FEAT] Add onboarding wizard with visura camerale and CertiSource integration - New 5-step onboarding wizard (onboarding.html) replacing setup-org.html - Step 1: Choose data source (Upload Visura / CertiSource / Manual) - Step 2: PDF upload with AI extraction or CertiSource P.IVA lookup - Step 3: Verify/complete company data with NIS2 sector mapping - Step 4: User profile completion - Step 5: NIS2 classification (Essential/Important) with summary - OnboardingController with upload-visura, fetch-company, complete endpoints - VisuraService with Claude AI PDF extraction and ATECO-to-NIS2 mapping - CertiSource API integration for automatic company data retrieval - Updated login/register redirects to point to new onboarding wizard Co-Authored-By: Claude Opus 4.6 --- .../controllers/OnboardingController.php | 332 ++++ application/services/VisuraService.php | 310 +++ public/index.php | 8 + public/js/api.js | 24 + public/login.html | 2 +- public/onboarding.html | 1661 +++++++++++++++++ public/register.html | 2 +- 7 files changed, 2337 insertions(+), 2 deletions(-) create mode 100644 application/controllers/OnboardingController.php create mode 100644 application/services/VisuraService.php create mode 100644 public/onboarding.html diff --git a/application/controllers/OnboardingController.php b/application/controllers/OnboardingController.php new file mode 100644 index 0000000..4e2e8b5 --- /dev/null +++ b/application/controllers/OnboardingController.php @@ -0,0 +1,332 @@ +requireAuth(); + + // Validate file upload + if (!isset($_FILES['visura']) || $_FILES['visura']['error'] !== UPLOAD_ERR_OK) { + $this->jsonError('Nessun file caricato o errore upload', 400, 'UPLOAD_ERROR'); + } + + $file = $_FILES['visura']; + + // Validate file type (PDF only) + $allowedTypes = ['application/pdf']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + + if (!in_array($mimeType, $allowedTypes)) { + $this->jsonError('Solo file PDF sono accettati', 400, 'INVALID_FILE_TYPE'); + } + + // Max 10MB + if ($file['size'] > 10 * 1024 * 1024) { + $this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE'); + } + + // Save file temporarily + $uploadDir = UPLOAD_PATH . '/visure'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + $fileName = 'visura_' . $this->getCurrentUserId() . '_' . time() . '.pdf'; + $filePath = $uploadDir . '/' . $fileName; + + if (!move_uploaded_file($file['tmp_name'], $filePath)) { + $this->jsonError('Errore nel salvataggio del file', 500, 'SAVE_ERROR'); + } + + try { + // Extract data using AI + $visuraService = new VisuraService(); + $extractedData = $visuraService->extractFromPdf($filePath); + + $this->logAudit('visura_uploaded', 'onboarding', null, [ + 'file' => $fileName, + 'extracted' => !empty($extractedData) + ]); + + $this->jsonSuccess($extractedData, 'Dati estratti dalla visura'); + } catch (Throwable $e) { + error_log('[VISURA_ERROR] ' . $e->getMessage()); + $this->jsonError( + APP_DEBUG ? 'Errore estrazione: ' . $e->getMessage() : 'Errore nell\'analisi della visura', + 500, + 'EXTRACTION_ERROR' + ); + } + } + + /** + * POST /api/onboarding/fetch-company + * Recupera dati aziendali da CertiSource tramite P.IVA + */ + public function fetchCompany(): void + { + $this->requireAuth(); + $this->validateRequired(['vat_number']); + + $vatNumber = trim($this->getParam('vat_number')); + + // Clean VAT: remove IT prefix, spaces + $vatNumber = preg_replace('/^IT/i', '', $vatNumber); + $vatNumber = preg_replace('/\s+/', '', $vatNumber); + + // Validate Italian P.IVA format (11 digits) + if (!preg_match('/^\d{11}$/', $vatNumber)) { + $this->jsonError('Formato Partita IVA non valido (11 cifre)', 400, 'INVALID_VAT'); + } + + try { + $visuraService = new VisuraService(); + $companyData = $visuraService->fetchFromCertiSource($vatNumber); + + $this->logAudit('certisource_fetch', 'onboarding', null, [ + 'vat_number' => $vatNumber, + 'found' => !empty($companyData) + ]); + + if (empty($companyData) || empty($companyData['company_name'])) { + $this->jsonError( + 'Azienda non trovata per la P.IVA fornita', + 404, + 'COMPANY_NOT_FOUND' + ); + } + + $this->jsonSuccess($companyData, 'Dati aziendali recuperati'); + } catch (Throwable $e) { + error_log('[CERTISOURCE_ERROR] ' . $e->getMessage()); + $this->jsonError( + APP_DEBUG ? 'Errore CertiSource: ' . $e->getMessage() : 'Errore nel recupero dati aziendali', + 500, + 'CERTISOURCE_ERROR' + ); + } + } + + /** + * POST /api/onboarding/complete + * Completa l'onboarding: crea organizzazione e aggiorna profilo utente + */ + public function complete(): void + { + $this->requireAuth(); + $this->validateRequired(['name', 'sector']); + + $userId = $this->getCurrentUserId(); + + // Check if user already has an organization + $existingOrg = Database::fetchOne( + 'SELECT organization_id FROM user_organizations WHERE user_id = ? AND is_primary = 1', + [$userId] + ); + + if ($existingOrg) { + $this->jsonError('Hai già un\'organizzazione configurata', 409, 'ORG_EXISTS'); + } + + Database::beginTransaction(); + try { + // Create organization + $orgData = [ + 'name' => trim($this->getParam('name')), + 'vat_number' => $this->getParam('vat_number'), + 'fiscal_code' => $this->getParam('fiscal_code'), + 'sector' => $this->getParam('sector'), + 'employee_count' => $this->getParam('employee_count'), + 'annual_turnover_eur' => $this->getParam('annual_turnover_eur'), + 'country' => $this->getParam('country', 'IT'), + 'city' => $this->getParam('city'), + 'address' => $this->getParam('address'), + 'website' => $this->getParam('website'), + 'contact_email' => $this->getParam('contact_email'), + 'contact_phone' => $this->getParam('contact_phone'), + ]; + + $orgId = Database::insert('organizations', $orgData); + + // Auto-classify NIS2 entity type + $entityType = $this->classifyNis2Entity( + $orgData['sector'], + (int) ($orgData['employee_count'] ?? 0), + (float) ($orgData['annual_turnover_eur'] ?? 0) + ); + + Database::update('organizations', [ + 'entity_type' => $entityType, + ], 'id = ?', [$orgId]); + + // Link user as org_admin + Database::insert('user_organizations', [ + 'user_id' => $userId, + 'organization_id' => $orgId, + 'role' => 'org_admin', + 'is_primary' => 1, + ]); + + // Update user profile if provided + $profileUpdates = []; + if ($this->hasParam('phone') && $this->getParam('phone')) { + $profileUpdates['phone'] = $this->getParam('phone'); + } + if ($this->hasParam('full_name') && $this->getParam('full_name')) { + $profileUpdates['full_name'] = trim($this->getParam('full_name')); + } + if (!empty($profileUpdates)) { + Database::update('users', $profileUpdates, 'id = ?', [$userId]); + } + + // Initialize NIS2 compliance controls + $this->initializeComplianceControls($orgId); + + Database::commit(); + + $this->currentOrgId = $orgId; + $this->logAudit('onboarding_completed', 'organization', $orgId, [ + 'name' => $orgData['name'], + 'sector' => $orgData['sector'], + 'entity_type' => $entityType + ]); + + $this->jsonSuccess([ + 'organization_id' => $orgId, + 'name' => $orgData['name'], + 'entity_type' => $entityType, + 'classification' => $this->getClassificationDetails($entityType, $orgData['sector'], (int)($orgData['employee_count'] ?? 0), (float)($orgData['annual_turnover_eur'] ?? 0)), + ], 'Onboarding completato', 201); + + } catch (Throwable $e) { + Database::rollback(); + throw $e; + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // PRIVATE METHODS (copied from OrganizationController for independence) + // ═══════════════════════════════════════════════════════════════════════ + + private function classifyNis2Entity(string $sector, int $employees, float $turnover): string + { + // Map detailed sector codes to NIS2 categories + $essentialSectors = [ + 'energy', 'energy_electricity', 'energy_district_heating', 'energy_oil', 'energy_gas', 'energy_hydrogen', + 'transport', 'transport_air', 'transport_rail', 'transport_water', 'transport_road', + 'banking', 'financial_markets', 'health', 'water', 'drinking_water', 'waste_water', + 'digital_infra', 'digital_infrastructure', 'ict_service_management', + 'public_admin', 'public_administration', 'space', + ]; + + $importantSectors = [ + 'manufacturing', 'manufacturing_medical', 'manufacturing_computers', 'manufacturing_electrical', + 'manufacturing_machinery', 'manufacturing_vehicles', 'manufacturing_transport', + 'postal', 'postal_courier', 'chemical', 'chemicals', 'food', + 'waste', 'waste_management', 'ict_services', 'digital_providers', 'research', + ]; + + $isLarge = $employees >= 250 || $turnover >= 50000000; + $isMedium = ($employees >= 50 || $turnover >= 10000000) && !$isLarge; + + if (in_array($sector, $essentialSectors)) { + if ($isLarge) return 'essential'; + if ($isMedium) return 'important'; + } + + if (in_array($sector, $importantSectors)) { + if ($isLarge || $isMedium) return 'important'; + } + + return 'not_applicable'; + } + + private function getClassificationDetails(string $type, string $sector, int $employees, float $turnover): array + { + $sizeLabel = $employees >= 250 ? 'Grande Impresa' : ($employees >= 50 ? 'Media Impresa' : 'Piccola Impresa'); + + return match ($type) { + 'essential' => [ + 'type' => 'essential', + 'label' => 'Soggetto Essenziale', + 'size' => $sizeLabel, + 'description' => "La vostra organizzazione rientra tra i soggetti essenziali ai sensi della Direttiva NIS2 (UE 2022/2555). Operate in un settore ad alta criticità e superate le soglie dimensionali.", + 'obligations' => [ + 'Misure di sicurezza informatica (Art. 21)', + 'Notifica incidenti significativi al CSIRT entro 24h/72h/30gg (Art. 23)', + 'Formazione obbligatoria per gli organi di gestione (Art. 20)', + 'Vigilanza proattiva (ex ante) da parte delle autorità', + 'Sanzioni fino a EUR 10M o 2% del fatturato mondiale annuo', + ], + ], + 'important' => [ + 'type' => 'important', + 'label' => 'Soggetto Importante', + 'size' => $sizeLabel, + 'description' => "La vostra organizzazione rientra tra i soggetti importanti ai sensi della Direttiva NIS2. Siete tenuti a rispettare gli obblighi di sicurezza e notifica incidenti.", + 'obligations' => [ + 'Misure di sicurezza informatica (Art. 21)', + 'Notifica incidenti significativi al CSIRT entro 24h/72h/30gg (Art. 23)', + 'Formazione obbligatoria per gli organi di gestione (Art. 20)', + 'Vigilanza reattiva (ex post) da parte delle autorità', + 'Sanzioni fino a EUR 7M o 1,4% del fatturato mondiale annuo', + ], + ], + default => [ + 'type' => 'not_applicable', + 'label' => 'Non Applicabile', + 'size' => $sizeLabel, + 'description' => "In base ai dati forniti, la vostra organizzazione non sembra rientrare nell'ambito di applicazione della Direttiva NIS2. Consigliamo comunque di adottare le best practice di cybersecurity.", + 'obligations' => [ + 'Nessun obbligo NIS2 specifico', + 'Si raccomandano comunque buone pratiche di sicurezza informatica', + 'Possibile designazione futura da parte delle autorità nazionali', + ], + ], + }; + } + + private function initializeComplianceControls(int $orgId): void + { + $controls = [ + ['NIS2-21.2.a', 'nis2', 'Politiche di analisi dei rischi e sicurezza dei sistemi informatici'], + ['NIS2-21.2.b', 'nis2', 'Gestione degli incidenti'], + ['NIS2-21.2.c', 'nis2', 'Continuità operativa e gestione delle crisi'], + ['NIS2-21.2.d', 'nis2', 'Sicurezza della catena di approvvigionamento'], + ['NIS2-21.2.e', 'nis2', 'Sicurezza acquisizione, sviluppo e manutenzione sistemi'], + ['NIS2-21.2.f', 'nis2', 'Politiche e procedure per valutare efficacia misure'], + ['NIS2-21.2.g', 'nis2', 'Pratiche di igiene informatica di base e formazione'], + ['NIS2-21.2.h', 'nis2', 'Politiche e procedure relative alla crittografia'], + ['NIS2-21.2.i', 'nis2', 'Sicurezza risorse umane, controllo accessi e gestione asset'], + ['NIS2-21.2.j', 'nis2', 'Autenticazione multi-fattore e comunicazioni sicure'], + ['NIS2-20.1', 'nis2', 'Governance: approvazione misure da parte degli organi di gestione'], + ['NIS2-20.2', 'nis2', 'Formazione obbligatoria per gli organi di gestione'], + ['NIS2-23.1', 'nis2', 'Notifica incidenti significativi al CSIRT (24h/72h/30gg)'], + ]; + + foreach ($controls as [$code, $framework, $title]) { + Database::insert('compliance_controls', [ + 'organization_id' => $orgId, + 'control_code' => $code, + 'framework' => $framework, + 'title' => $title, + 'status' => 'not_started', + ]); + } + } +} diff --git a/application/services/VisuraService.php b/application/services/VisuraService.php new file mode 100644 index 0000000..f4194e8 --- /dev/null +++ b/application/services/VisuraService.php @@ -0,0 +1,310 @@ +callClaudeApi([ + [ + 'type' => 'document', + 'source' => [ + 'type' => 'base64', + 'media_type' => 'application/pdf', + 'data' => $base64Pdf, + ], + ], + [ + 'type' => 'text', + 'text' => "Analizza questa visura camerale italiana ed estrai i seguenti dati in formato JSON. Rispondi SOLO con il JSON, senza testo aggiuntivo, senza markdown code blocks.\n\nCampi da estrarre:\n- company_name: ragione sociale completa\n- vat_number: partita IVA (solo numeri, senza prefisso IT)\n- fiscal_code: codice fiscale\n- legal_form: forma giuridica (es. S.R.L., S.P.A., ecc.)\n- address: indirizzo sede legale (via/piazza e numero civico)\n- city: comune sede legale\n- province: sigla provincia (es. MI, RM, TO)\n- zip_code: CAP\n- pec: indirizzo PEC se presente\n- phone: telefono se presente\n- ateco_code: codice ATECO principale se presente\n- ateco_description: descrizione attività ATECO se presente\n- incorporation_date: data di costituzione (formato YYYY-MM-DD)\n- share_capital: capitale sociale in EUR (solo numero)\n- employees_range: stima range dipendenti se indicato (es. \"10-49\", \"50-249\", \"250+\")\n- legal_representative: nome e cognome del legale rappresentante\n\nSe un campo non è presente nella visura, usa null come valore.", + ], + ]); + + if (!$response) { + throw new RuntimeException('Nessuna risposta dall\'AI'); + } + + // Parse JSON response + $jsonStr = trim($response); + // Remove potential markdown code blocks + $jsonStr = preg_replace('/^```(?:json)?\s*/i', '', $jsonStr); + $jsonStr = preg_replace('/\s*```$/', '', $jsonStr); + + $data = json_decode($jsonStr, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + error_log('[VISURA_PARSE_ERROR] Could not parse AI response: ' . $jsonStr); + throw new RuntimeException('Impossibile interpretare i dati estratti dalla visura'); + } + + // Map to suggested NIS2 sector based on ATECO code + $data['suggested_sector'] = $this->mapAtecoToNis2Sector($data['ateco_code'] ?? '', $data['ateco_description'] ?? ''); + + // Log AI interaction + $this->logAiInteraction('visura_extraction', 'Estrazione dati da visura camerale PDF'); + + return $data; + } + + /** + * Fetch company data from CertiSource API + */ + public function fetchFromCertiSource(string $vatNumber): array + { + // CertiSource is on the same server - call its API internally + $certisourceUrl = $this->getCertiSourceBaseUrl() . '/api/company/enrich'; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $certisourceUrl . '?vat=' . urlencode($vatNumber), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Accept: application/json', + 'X-Internal-Service: nis2-agile', + ], + // Same server, skip SSL verification for internal calls + CURLOPT_SSL_VERIFYPEER => false, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + error_log("[CERTISOURCE_CURL_ERROR] $error"); + throw new RuntimeException('Impossibile contattare CertiSource: ' . $error); + } + + if ($httpCode !== 200) { + error_log("[CERTISOURCE_HTTP_ERROR] HTTP $httpCode: $response"); + throw new RuntimeException('CertiSource ha restituito un errore (HTTP ' . $httpCode . ')'); + } + + $result = json_decode($response, true); + if (!$result) { + throw new RuntimeException('Risposta CertiSource non valida'); + } + + // Map CertiSource response to our format + // CertiSource typically returns data in its own format, normalize it + $companyData = $result['data'] ?? $result; + + return [ + 'company_name' => $companyData['ragione_sociale'] ?? $companyData['denominazione'] ?? $companyData['company_name'] ?? null, + 'vat_number' => $companyData['partita_iva'] ?? $companyData['vat_number'] ?? $vatNumber, + 'fiscal_code' => $companyData['codice_fiscale'] ?? $companyData['fiscal_code'] ?? null, + 'legal_form' => $companyData['forma_giuridica'] ?? $companyData['legal_form'] ?? null, + 'address' => $companyData['indirizzo'] ?? $companyData['address'] ?? null, + 'city' => $companyData['comune'] ?? $companyData['city'] ?? null, + 'province' => $companyData['provincia'] ?? $companyData['province'] ?? null, + 'zip_code' => $companyData['cap'] ?? $companyData['zip_code'] ?? null, + 'pec' => $companyData['pec'] ?? null, + 'phone' => $companyData['telefono'] ?? $companyData['phone'] ?? null, + 'ateco_code' => $companyData['codice_ateco'] ?? $companyData['ateco_code'] ?? null, + 'ateco_description' => $companyData['descrizione_ateco'] ?? $companyData['ateco_description'] ?? null, + 'suggested_sector' => $this->mapAtecoToNis2Sector( + $companyData['codice_ateco'] ?? '', + $companyData['descrizione_ateco'] ?? '' + ), + 'source' => 'certisource', + ]; + } + + /** + * Map ATECO code to NIS2 sector + */ + private function mapAtecoToNis2Sector(string $atecoCode, string $atecoDesc): ?string + { + $code = substr($atecoCode, 0, 2); // Use first 2 digits + $descLower = strtolower($atecoDesc); + + // ATECO to NIS2 mapping (approximate) + $mapping = [ + '35' => 'energy_electricity', // Electricity, gas, steam + '49' => 'transport_road', // Land transport + '50' => 'transport_water', // Water transport + '51' => 'transport_air', // Air transport + '64' => 'banking', // Financial services + '65' => 'banking', // Insurance + '66' => 'financial_markets', // Financial auxiliaries + '86' => 'health', // Health + '36' => 'drinking_water', // Water supply + '37' => 'waste_water', // Sewerage + '38' => 'waste_management', // Waste management + '61' => 'digital_infrastructure', // Telecommunications + '62' => 'ict_service_management', // IT services + '63' => 'digital_providers', // Information services + '84' => 'public_administration', // Public admin + '53' => 'postal_courier', // Postal services + '20' => 'chemicals', // Chemicals manufacturing + '10' => 'food', // Food manufacturing + '11' => 'food', // Beverages + '21' => 'manufacturing_medical', // Pharma/medical + '26' => 'manufacturing_computers', // Electronics + '27' => 'manufacturing_electrical', // Electrical equipment + '28' => 'manufacturing_machinery', // Machinery + '29' => 'manufacturing_vehicles', // Motor vehicles + '30' => 'manufacturing_transport', // Other transport + '72' => 'research', // Scientific research + ]; + + if (isset($mapping[$code])) { + return $mapping[$code]; + } + + // Try to match by description keywords + $keywords = [ + 'energia' => 'energy_electricity', + 'elettric' => 'energy_electricity', + 'gas' => 'energy_gas', + 'petroli' => 'energy_oil', + 'trasport' => 'transport_road', + 'ferrov' => 'transport_rail', + 'maritt' => 'transport_water', + 'aere' => 'transport_air', + 'banc' => 'banking', + 'finanz' => 'financial_markets', + 'sanit' => 'health', + 'osped' => 'health', + 'farm' => 'manufacturing_medical', + 'acqua' => 'drinking_water', + 'rifiut' => 'waste_management', + 'telecom' => 'digital_infrastructure', + 'informatica' => 'ict_service_management', + 'software' => 'ict_service_management', + 'digital' => 'digital_providers', + 'postale' => 'postal_courier', + 'corriere' => 'postal_courier', + 'chimic' => 'chemicals', + 'alimentar' => 'food', + 'ricerca' => 'research', + ]; + + foreach ($keywords as $kw => $sector) { + if (str_contains($descLower, $kw)) { + return $sector; + } + } + + return null; + } + + /** + * Call Claude API + */ + private function callClaudeApi(array $content): ?string + { + $payload = [ + 'model' => ANTHROPIC_MODEL, + 'max_tokens' => ANTHROPIC_MAX_TOKENS, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $content, + ], + ], + ]; + + $ch = curl_init('https://api.anthropic.com/v1/messages'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'x-api-key: ' . ANTHROPIC_API_KEY, + 'anthropic-version: 2023-06-01', + ], + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + throw new RuntimeException('Claude API error: ' . $error); + } + + if ($httpCode !== 200) { + error_log("[CLAUDE_API_ERROR] HTTP $httpCode: $response"); + throw new RuntimeException('Claude API returned HTTP ' . $httpCode); + } + + $result = json_decode($response, true); + + return $result['content'][0]['text'] ?? null; + } + + /** + * Get CertiSource API base URL (same server) + */ + private function getCertiSourceBaseUrl(): string + { + // Both apps are on the same server, use internal URL + if (defined('CERTISOURCE_API_URL')) { + return CERTISOURCE_API_URL; + } + // Default: same server via localhost + return 'https://certisource.it/certisource'; + } + + /** + * Log AI interaction to database + */ + private function logAiInteraction(string $type, string $summary): void + { + try { + // Get current user from JWT if available + $userId = null; + $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; + if (preg_match('/Bearer\s+(.+)$/i', $token, $matches)) { + $parts = explode('.', $matches[1]); + if (count($parts) === 3) { + $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true); + $userId = $payload['sub'] ?? null; + } + } + + if ($userId) { + Database::insert('ai_interactions', [ + 'organization_id' => 0, // Not yet created during onboarding + 'user_id' => $userId, + 'interaction_type' => 'qa', + 'prompt_summary' => $summary, + 'response_summary' => 'Dati estratti', + 'tokens_used' => 0, + 'model_used' => ANTHROPIC_MODEL, + ]); + } + } catch (Throwable $e) { + // Silently fail - logging should not break the flow + error_log('[AI_LOG_ERROR] ' . $e->getMessage()); + } + } +} diff --git a/public/index.php b/public/index.php index 6d3f70a..fe560d3 100644 --- a/public/index.php +++ b/public/index.php @@ -97,6 +97,7 @@ $controllerMap = [ 'assets' => 'AssetController', 'audit' => 'AuditController', 'admin' => 'AdminController', + 'onboarding' => 'OnboardingController', ]; if (!isset($controllerMap[$controllerName])) { @@ -265,6 +266,13 @@ $actionMap = [ 'GET:users' => 'listUsers', 'GET:stats' => 'platformStats', ], + + // ── OnboardingController ────────────────────── + 'onboarding' => [ + 'POST:uploadVisura' => 'uploadVisura', + 'POST:fetchCompany' => 'fetchCompany', + 'POST:complete' => 'complete', + ], ]; // ═══════════════════════════════════════════════════════════════════════════ diff --git a/public/js/api.js b/public/js/api.js index 0aaa4da..bb3e2b7 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -238,6 +238,30 @@ class NIS2API { generateComplianceReport() { return this.get('/audit/report'); } getAuditLogs(params = {}) { return this.get('/audit/logs?' + new URLSearchParams(params)); } getIsoMapping() { return this.get('/audit/iso27001-mapping'); } + + // ═══════════════════════════════════════════════════════════════════ + // Onboarding + // ═══════════════════════════════════════════════════════════════════ + + async uploadVisura(file) { + const formData = new FormData(); + formData.append('visura', file); + const headers = { 'Authorization': 'Bearer ' + this.token }; + if (this.orgId) headers['X-Organization-Id'] = this.orgId; + try { + const response = await fetch(this.baseUrl + '/onboarding/upload-visura', { + method: 'POST', + headers, + body: formData, + }); + return response.json(); + } catch (error) { + return { success: false, message: 'Errore di connessione al server' }; + } + } + + fetchCompany(vatNumber) { return this.post('/onboarding/fetch-company', { vat_number: vatNumber }); } + completeOnboarding(data) { return this.post('/onboarding/complete', data); } } // Singleton globale diff --git a/public/login.html b/public/login.html index 3cf9222..fdef405 100644 --- a/public/login.html +++ b/public/login.html @@ -86,7 +86,7 @@ if (result.data.organizations && result.data.organizations.length > 0) { window.location.href = 'dashboard.html'; } else { - window.location.href = 'setup-org.html'; + window.location.href = 'onboarding.html'; } } else { errorEl.textContent = result.message || 'Credenziali non valide.'; diff --git a/public/onboarding.html b/public/onboarding.html new file mode 100644 index 0000000..df91663 --- /dev/null +++ b/public/onboarding.html @@ -0,0 +1,1661 @@ + + + + + + Onboarding - NIS2 Agile + + + + +
+ +
+ +
+

NIS2 Agile

+ Configurazione Iniziale +
+
+ + +
+
+
+
1
+ Benvenuto +
+
+
+
2
+ Acquisizione Dati +
+
+
+
3
+ Dati Aziendali +
+
+
+
4
+ Il Tuo Profilo +
+
+
+
5
+ Classificazione +
+
+
+ + +
+
+ + +
+

Configura il tuo ambiente NIS2

+

Per iniziare, abbiamo bisogno dei dati della tua azienda. Puoi fornirli in tre modi:

+ +
+ +
+
+ +
+
+ + + + + + +
+
Carica Visura Camerale
+
Carica il PDF della visura camerale e i dati verranno estratti automaticamente con l'AI
+
+ + +
+
+ +
+
+ + + + + + +
+
Recupera con CertiSource
+
Inserisci la Partita IVA e recupereremo automaticamente i dati aziendali tramite CertiSource
+
+ + +
+
+ +
+
+ + + + +
+
Inserisci Manualmente
+
Compila tu stesso i dati dell'azienda
+
+
+ + +
+ + +
+ + + + + + +
+ + +
+

Verifica e Completa i Dati Aziendali

+

Controlla i dati qui sotto e completa le informazioni mancanti.

+ +
+ + +
+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+ +
+ +
+
+
+ + +
+

Completa il tuo profilo

+

Inserisci le informazioni sul tuo ruolo in azienda.

+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+ +
+ +
+
+
+ + +
+

Classificazione NIS2

+

In base ai dati forniti, ecco la classificazione della tua organizzazione secondo la Direttiva NIS2.

+ + +
+
+ + + +
+
+
+
+ + +
+
Riepilogo Dati Aziendali
+
+
+
+
Ragione Sociale
+
-
+
+
+
Partita IVA
+
-
+
+
+
Settore
+
-
+
+
+
Tipologia Soggetto
+
-
+
+
+
Dipendenti
+
-
+
+
+
Fatturato Annuo
+
-
+
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+ + + + + + diff --git a/public/register.html b/public/register.html index d02b551..cb46312 100644 --- a/public/register.html +++ b/public/register.html @@ -160,7 +160,7 @@ showNotification('Account creato con successo!', 'success'); // Dopo la registrazione, porta al setup organizzazione setTimeout(() => { - window.location.href = 'setup-org.html'; + window.location.href = 'onboarding.html'; }, 500); } else { errorEl.textContent = result.message || 'Errore durante la registrazione.';