[CORE] Initial project scaffold - NIS2 Agile Compliance Platform
Complete MVP implementation including: - PHP 8.4 backend with Front Controller pattern (80+ API endpoints) - Multi-tenant architecture with organization_id isolation - JWT authentication (HS256, 2h access + 7d refresh tokens) - 14 controllers: Auth, Organization, Assessment, Dashboard, Risk, Incident, Policy, SupplyChain, Training, Asset, Audit, Admin - AI Service integration (Anthropic Claude API) for gap analysis, risk suggestions, policy generation, incident classification - NIS2 gap analysis questionnaire (~80 questions, 10 categories) - MySQL schema (20 tables) with NIS2 Art. 21 compliance controls - NIS2 Art. 23 incident reporting workflow (24h/72h/30d) - Frontend: login, register, dashboard, assessment wizard, org setup - Docker configuration (PHP-FPM + Nginx + MySQL) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
RewriteEngine On
|
||||
|
||||
# Redirect HTTP to HTTPS (production)
|
||||
# RewriteCond %{HTTPS} off
|
||||
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# If file or directory exists, serve directly
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# Route everything through index.php
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Health Check
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$status = [
|
||||
'status' => 'ok',
|
||||
'app' => 'NIS2 Agile',
|
||||
'version' => '1.0.0',
|
||||
'timestamp' => date('c'),
|
||||
'checks' => [],
|
||||
];
|
||||
|
||||
// Database check
|
||||
try {
|
||||
require_once __DIR__ . '/../application/config/config.php';
|
||||
require_once __DIR__ . '/../application/config/database.php';
|
||||
|
||||
Database::fetchOne('SELECT 1');
|
||||
$status['checks']['database'] = 'ok';
|
||||
} catch (Throwable $e) {
|
||||
$status['checks']['database'] = 'error';
|
||||
$status['status'] = 'degraded';
|
||||
}
|
||||
|
||||
http_response_code($status['status'] === 'ok' ? 200 : 503);
|
||||
echo json_encode($status, JSON_PRETTY_PRINT);
|
||||
@@ -0,0 +1,585 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gap Analysis - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2>Gap Analysis</h2>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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>
|
||||
Analisi AI
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Assessment Selection / Create -->
|
||||
<div id="assessment-start" class="card mb-24">
|
||||
<div class="card-body text-center" style="padding:48px;">
|
||||
<h3 style="font-size:1.25rem; color:var(--gray-900); margin-bottom:8px;">Assessment di Conformita' NIS2</h3>
|
||||
<p class="text-muted mb-24">Valuta il livello di conformita' della tua organizzazione rispetto ai requisiti della direttiva NIS2.</p>
|
||||
|
||||
<div id="existing-assessments" class="mb-24" style="display:none;"></div>
|
||||
|
||||
<button class="btn btn-primary btn-lg" id="btn-new-assessment" onclick="createNewAssessment()">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
||||
Nuovo Assessment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard Container -->
|
||||
<div id="assessment-wizard" style="display:none;">
|
||||
<!-- Steps Indicator -->
|
||||
<div class="steps" id="steps-indicator"></div>
|
||||
|
||||
<!-- Question Card -->
|
||||
<div class="card mb-24">
|
||||
<div class="card-header">
|
||||
<h3 id="category-title">Caricamento...</h3>
|
||||
<span class="badge badge-neutral" id="question-counter">0/0</span>
|
||||
</div>
|
||||
<div class="card-body" id="question-area">
|
||||
<div class="spinner-lg" style="margin:40px auto;"></div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<button class="btn btn-secondary" id="btn-prev" onclick="prevQuestion()" disabled>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
||||
Precedente
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="saveCurrentResponse()">
|
||||
Salva Progresso
|
||||
</button>
|
||||
<button class="btn btn-primary" id="btn-next" onclick="nextQuestion()">
|
||||
Successiva
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Container -->
|
||||
<div id="assessment-results" style="display:none;">
|
||||
<div class="card mb-24">
|
||||
<div class="card-header">
|
||||
<h3>Risultati Assessment</h3>
|
||||
<button class="btn btn-outline btn-sm" onclick="aiAnalyze()">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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>
|
||||
Analisi AI
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Overall Score -->
|
||||
<div class="text-center mb-32" id="results-gauge"></div>
|
||||
|
||||
<!-- Category Scores -->
|
||||
<h4 style="margin-bottom:16px; color:var(--gray-900);">Punteggio per Categoria</h4>
|
||||
<div id="category-scores"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Analysis result -->
|
||||
<div class="card mb-24" id="ai-analysis-card" style="display:none;">
|
||||
<div class="card-header">
|
||||
<h3>Analisi AI</h3>
|
||||
<span class="badge badge-info">Intelligenza Artificiale</span>
|
||||
</div>
|
||||
<div class="card-body" id="ai-analysis-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
// ── State ────────────────────────────────────────────────
|
||||
let currentAssessmentId = null;
|
||||
let questions = [];
|
||||
let categories = [];
|
||||
let currentCategoryIdx = 0;
|
||||
let currentQuestionIdx = 0;
|
||||
let responses = {}; // questionId -> { answer, maturity, notes, evidence }
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────
|
||||
loadAssessments();
|
||||
|
||||
async function loadAssessments() {
|
||||
try {
|
||||
const result = await api.listAssessments();
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
const container = document.getElementById('existing-assessments');
|
||||
container.style.display = 'block';
|
||||
|
||||
let html = '<h4 style="margin-bottom:12px; color:var(--gray-700);">Assessment Esistenti</h4>';
|
||||
html += '<div class="table-container"><table><thead><tr><th>Data</th><th>Stato</th><th>Punteggio</th><th>Azioni</th></tr></thead><tbody>';
|
||||
|
||||
result.data.forEach(a => {
|
||||
const statusBadge = a.status === 'completed'
|
||||
? '<span class="badge badge-success">Completato</span>'
|
||||
: '<span class="badge badge-warning">In Corso</span>';
|
||||
html += `
|
||||
<tr>
|
||||
<td>${formatDate(a.created_at)}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${a.overall_score != null ? Math.round(a.overall_score) + '%' : '-'}</td>
|
||||
<td>
|
||||
${a.status === 'completed'
|
||||
? `<button class="btn btn-sm btn-ghost" onclick="viewResults('${a.id}')">Risultati</button>`
|
||||
: `<button class="btn btn-sm btn-primary" onclick="resumeAssessment('${a.id}')">Riprendi</button>`
|
||||
}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} catch (e) {
|
||||
// Silenzioso
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewAssessment() {
|
||||
const btn = document.getElementById('btn-new-assessment');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creazione in corso...';
|
||||
|
||||
try {
|
||||
const result = await api.createAssessment({ title: 'Assessment NIS2 - ' + formatDate(new Date()) });
|
||||
if (result.success && result.data) {
|
||||
currentAssessmentId = result.data.id;
|
||||
await loadQuestions();
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nella creazione.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg> Nuovo Assessment`;
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeAssessment(id) {
|
||||
currentAssessmentId = id;
|
||||
await loadQuestions();
|
||||
}
|
||||
|
||||
async function viewResults(id) {
|
||||
currentAssessmentId = id;
|
||||
await showResults();
|
||||
}
|
||||
|
||||
async function loadQuestions() {
|
||||
try {
|
||||
const result = await api.getAssessmentQuestions(currentAssessmentId);
|
||||
if (result.success && result.data) {
|
||||
questions = result.data.questions || result.data;
|
||||
organizeByCategory();
|
||||
showWizard();
|
||||
renderCurrentQuestion();
|
||||
} else {
|
||||
showNotification('Errore nel caricamento delle domande.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function organizeByCategory() {
|
||||
const catMap = {};
|
||||
questions.forEach(q => {
|
||||
const cat = q.category || 'Generale';
|
||||
if (!catMap[cat]) catMap[cat] = [];
|
||||
catMap[cat].push(q);
|
||||
});
|
||||
categories = Object.keys(catMap).map(name => ({
|
||||
name,
|
||||
questions: catMap[name]
|
||||
}));
|
||||
|
||||
// Ripristina risposte precedenti
|
||||
questions.forEach(q => {
|
||||
if (q.response) {
|
||||
responses[q.id] = {
|
||||
answer: q.response.answer || q.response.compliance_level,
|
||||
maturity: q.response.maturity_level,
|
||||
notes: q.response.notes || '',
|
||||
evidence: q.response.evidence_description || ''
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showWizard() {
|
||||
document.getElementById('assessment-start').style.display = 'none';
|
||||
document.getElementById('assessment-wizard').style.display = 'block';
|
||||
document.getElementById('assessment-results').style.display = 'none';
|
||||
renderSteps();
|
||||
}
|
||||
|
||||
function renderSteps() {
|
||||
const container = document.getElementById('steps-indicator');
|
||||
let html = '';
|
||||
categories.forEach((cat, i) => {
|
||||
const cls = i === currentCategoryIdx ? 'active' : i < currentCategoryIdx ? 'completed' : '';
|
||||
html += `<div class="step ${cls}" onclick="goToCategory(${i})">
|
||||
<span class="step-number">${i + 1}</span>
|
||||
<span class="step-label">${escapeHtml(cat.name)}</span>
|
||||
</div>`;
|
||||
if (i < categories.length - 1) {
|
||||
html += `<div class="step-connector"></div>`;
|
||||
}
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function goToCategory(idx) {
|
||||
if (idx <= currentCategoryIdx || idx === currentCategoryIdx + 1) {
|
||||
saveCurrentResponseSilent();
|
||||
currentCategoryIdx = idx;
|
||||
currentQuestionIdx = 0;
|
||||
renderSteps();
|
||||
renderCurrentQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
function renderCurrentQuestion() {
|
||||
if (categories.length === 0) return;
|
||||
|
||||
const cat = categories[currentCategoryIdx];
|
||||
const q = cat.questions[currentQuestionIdx];
|
||||
const globalIdx = getGlobalQuestionIndex();
|
||||
const totalQuestions = questions.length;
|
||||
|
||||
document.getElementById('category-title').textContent = cat.name;
|
||||
document.getElementById('question-counter').textContent = `${globalIdx + 1}/${totalQuestions}`;
|
||||
|
||||
const r = responses[q.id] || {};
|
||||
|
||||
const answers = [
|
||||
{ value: 'non_implementato', label: 'Non Implementato', cls: 'danger' },
|
||||
{ value: 'parziale', label: 'Parziale', cls: 'warning' },
|
||||
{ value: 'implementato', label: 'Implementato', cls: 'success' },
|
||||
{ value: 'non_applicabile', label: 'Non Applicabile', cls: 'neutral' },
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="margin-bottom:20px;">
|
||||
<p style="font-size:1rem; font-weight:600; color:var(--gray-900); margin-bottom:4px;">
|
||||
${escapeHtml(q.text || q.question || q.title || '')}
|
||||
</p>
|
||||
${q.description ? `<p class="text-muted" style="font-size:0.8125rem;">${escapeHtml(q.description)}</p>` : ''}
|
||||
${q.reference ? `<span class="tag mt-8">Rif: ${escapeHtml(q.reference)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Livello di Conformita'</label>
|
||||
<div class="form-radio-group">
|
||||
${answers.map(a => `
|
||||
<label class="form-radio ${r.answer === a.value ? 'selected' : ''}">
|
||||
<input type="radio" name="answer" value="${a.value}" ${r.answer === a.value ? 'checked' : ''}
|
||||
onchange="onAnswerChange(this)">
|
||||
${a.label}
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Livello di Maturita' (0-5)</label>
|
||||
<input type="range" class="form-range" min="0" max="5" step="1"
|
||||
value="${r.maturity != null ? r.maturity : 0}" id="maturity-slider"
|
||||
oninput="document.getElementById('maturity-value').textContent = this.value">
|
||||
<div class="form-range-labels">
|
||||
<span>0 - Inesistente</span>
|
||||
<span id="maturity-value">${r.maturity != null ? r.maturity : 0}</span>
|
||||
<span>5 - Ottimizzato</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Note</label>
|
||||
<textarea class="form-textarea" id="question-notes" rows="3"
|
||||
placeholder="Aggiungi eventuali note o osservazioni...">${escapeHtml(r.notes || '')}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Descrizione Evidenza</label>
|
||||
<input type="text" class="form-input" id="question-evidence"
|
||||
placeholder="Documenti, procedure, strumenti a supporto..."
|
||||
value="${escapeHtml(r.evidence || '')}">
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('question-area').innerHTML = html;
|
||||
|
||||
// Navigation buttons
|
||||
document.getElementById('btn-prev').disabled = (currentCategoryIdx === 0 && currentQuestionIdx === 0);
|
||||
|
||||
const isLast = currentCategoryIdx === categories.length - 1 && currentQuestionIdx === cat.questions.length - 1;
|
||||
const nextBtn = document.getElementById('btn-next');
|
||||
if (isLast) {
|
||||
nextBtn.innerHTML = 'Completa Assessment';
|
||||
nextBtn.className = 'btn btn-success';
|
||||
} else {
|
||||
nextBtn.innerHTML = 'Successiva <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>';
|
||||
nextBtn.className = 'btn btn-primary';
|
||||
}
|
||||
}
|
||||
|
||||
function onAnswerChange(radio) {
|
||||
// Highlight selected radio
|
||||
document.querySelectorAll('.form-radio').forEach(el => el.classList.remove('selected'));
|
||||
radio.closest('.form-radio').classList.add('selected');
|
||||
}
|
||||
|
||||
function getGlobalQuestionIndex() {
|
||||
let idx = 0;
|
||||
for (let i = 0; i < currentCategoryIdx; i++) {
|
||||
idx += categories[i].questions.length;
|
||||
}
|
||||
return idx + currentQuestionIdx;
|
||||
}
|
||||
|
||||
function saveCurrentResponseSilent() {
|
||||
if (categories.length === 0) return;
|
||||
|
||||
const cat = categories[currentCategoryIdx];
|
||||
const q = cat.questions[currentQuestionIdx];
|
||||
const selectedRadio = document.querySelector('input[name="answer"]:checked');
|
||||
const maturity = document.getElementById('maturity-slider');
|
||||
const notes = document.getElementById('question-notes');
|
||||
const evidence = document.getElementById('question-evidence');
|
||||
|
||||
responses[q.id] = {
|
||||
answer: selectedRadio ? selectedRadio.value : null,
|
||||
maturity: maturity ? parseInt(maturity.value) : 0,
|
||||
notes: notes ? notes.value : '',
|
||||
evidence: evidence ? evidence.value : ''
|
||||
};
|
||||
}
|
||||
|
||||
async function saveCurrentResponse() {
|
||||
saveCurrentResponseSilent();
|
||||
const cat = categories[currentCategoryIdx];
|
||||
const q = cat.questions[currentQuestionIdx];
|
||||
const r = responses[q.id];
|
||||
|
||||
if (!r || !r.answer) {
|
||||
showNotification('Seleziona un livello di conformita\' prima di salvare.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.saveAssessmentResponse(currentAssessmentId, {
|
||||
question_id: q.id,
|
||||
compliance_level: r.answer,
|
||||
maturity_level: r.maturity,
|
||||
notes: r.notes,
|
||||
evidence_description: r.evidence
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
showNotification('Risposta salvata.', 'success', 2000);
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nel salvataggio.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function nextQuestion() {
|
||||
saveCurrentResponseSilent();
|
||||
|
||||
// Salva la risposta corrente
|
||||
const cat = categories[currentCategoryIdx];
|
||||
const q = cat.questions[currentQuestionIdx];
|
||||
const r = responses[q.id];
|
||||
|
||||
if (r && r.answer) {
|
||||
// Salva in background
|
||||
api.saveAssessmentResponse(currentAssessmentId, {
|
||||
question_id: q.id,
|
||||
compliance_level: r.answer,
|
||||
maturity_level: r.maturity,
|
||||
notes: r.notes,
|
||||
evidence_description: r.evidence
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Naviga avanti
|
||||
const isLastInCategory = currentQuestionIdx >= cat.questions.length - 1;
|
||||
const isLastCategory = currentCategoryIdx >= categories.length - 1;
|
||||
|
||||
if (isLastInCategory && isLastCategory) {
|
||||
// Completa l'assessment
|
||||
await completeAssessment();
|
||||
} else if (isLastInCategory) {
|
||||
currentCategoryIdx++;
|
||||
currentQuestionIdx = 0;
|
||||
renderSteps();
|
||||
renderCurrentQuestion();
|
||||
} else {
|
||||
currentQuestionIdx++;
|
||||
renderCurrentQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
function prevQuestion() {
|
||||
saveCurrentResponseSilent();
|
||||
|
||||
if (currentQuestionIdx > 0) {
|
||||
currentQuestionIdx--;
|
||||
} else if (currentCategoryIdx > 0) {
|
||||
currentCategoryIdx--;
|
||||
currentQuestionIdx = categories[currentCategoryIdx].questions.length - 1;
|
||||
}
|
||||
|
||||
renderSteps();
|
||||
renderCurrentQuestion();
|
||||
}
|
||||
|
||||
async function completeAssessment() {
|
||||
try {
|
||||
const result = await api.completeAssessment(currentAssessmentId);
|
||||
if (result.success) {
|
||||
showNotification('Assessment completato!', 'success');
|
||||
await showResults();
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nel completamento.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function showResults() {
|
||||
document.getElementById('assessment-start').style.display = 'none';
|
||||
document.getElementById('assessment-wizard').style.display = 'none';
|
||||
document.getElementById('assessment-results').style.display = 'block';
|
||||
document.getElementById('btn-ai-analyze').style.display = '';
|
||||
|
||||
try {
|
||||
const result = await api.getAssessmentReport(currentAssessmentId);
|
||||
if (result.success && result.data) {
|
||||
const report = result.data;
|
||||
|
||||
// Overall score gauge
|
||||
const score = report.overall_score || 0;
|
||||
document.getElementById('results-gauge').innerHTML = renderScoreGauge(score, 200);
|
||||
|
||||
// Category scores
|
||||
const catScores = report.category_scores || report.categories || {};
|
||||
let scoresHtml = '';
|
||||
|
||||
const entries = Array.isArray(catScores) ? catScores : Object.entries(catScores).map(([k, v]) => ({
|
||||
name: k,
|
||||
score: typeof v === 'object' ? v.score : v
|
||||
}));
|
||||
|
||||
entries.forEach(cat => {
|
||||
const catName = cat.name || cat.category;
|
||||
const catScore = Math.round(cat.score || 0);
|
||||
const color = getScoreColor(catScore);
|
||||
const barClass = catScore >= 60 ? 'success' : catScore >= 40 ? 'warning' : 'danger';
|
||||
|
||||
scoresHtml += `
|
||||
<div class="category-score">
|
||||
<div class="category-score-header">
|
||||
<span class="category-score-name">${escapeHtml(catName)}</span>
|
||||
<span class="category-score-value" style="color:${color}">${catScore}%</span>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar ${barClass}" style="width:${catScore}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById('category-scores').innerHTML = scoresHtml || '<p class="text-muted">Nessun dato disponibile per le categorie.</p>';
|
||||
} else {
|
||||
document.getElementById('results-gauge').innerHTML = renderScoreGauge(0, 200);
|
||||
document.getElementById('category-scores').innerHTML = '<p class="text-muted">Report non ancora disponibile.</p>';
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore nel caricamento dei risultati.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function aiAnalyze() {
|
||||
if (!currentAssessmentId) {
|
||||
showNotification('Nessun assessment selezionato.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showNotification('Analisi AI in corso... Potrebbe richiedere qualche secondo.', 'info', 6000);
|
||||
|
||||
try {
|
||||
const result = await api.aiAnalyzeAssessment(currentAssessmentId);
|
||||
if (result.success && result.data) {
|
||||
const card = document.getElementById('ai-analysis-card');
|
||||
const content = document.getElementById('ai-analysis-content');
|
||||
card.style.display = 'block';
|
||||
|
||||
const analysis = result.data;
|
||||
let html = '';
|
||||
|
||||
if (analysis.summary) {
|
||||
html += `<div style="margin-bottom:20px;"><h4 style="margin-bottom:8px;">Riepilogo</h4><p style="color:var(--gray-600); line-height:1.7;">${escapeHtml(analysis.summary)}</p></div>`;
|
||||
}
|
||||
|
||||
if (analysis.strengths && analysis.strengths.length > 0) {
|
||||
html += `<div style="margin-bottom:20px;"><h4 style="margin-bottom:8px; color:var(--secondary);">Punti di Forza</h4><ul style="padding-left:20px; color:var(--gray-600);">`;
|
||||
analysis.strengths.forEach(s => { html += `<li style="margin-bottom:4px;">${escapeHtml(s)}</li>`; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
if (analysis.weaknesses && analysis.weaknesses.length > 0) {
|
||||
html += `<div style="margin-bottom:20px;"><h4 style="margin-bottom:8px; color:var(--danger);">Aree di Miglioramento</h4><ul style="padding-left:20px; color:var(--gray-600);">`;
|
||||
analysis.weaknesses.forEach(w => { html += `<li style="margin-bottom:4px;">${escapeHtml(w)}</li>`; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
if (analysis.recommendations && analysis.recommendations.length > 0) {
|
||||
html += `<div><h4 style="margin-bottom:8px; color:var(--primary);">Raccomandazioni</h4><ol style="padding-left:20px; color:var(--gray-600);">`;
|
||||
analysis.recommendations.forEach(r => { html += `<li style="margin-bottom:8px;">${escapeHtml(r)}</li>`; });
|
||||
html += '</ol></div>';
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = `<p style="color:var(--gray-600); line-height:1.7;">${escapeHtml(analysis.text || analysis.message || JSON.stringify(analysis))}</p>`;
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
showNotification('Analisi AI completata.', 'success');
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nell\'analisi AI.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,324 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2>Dashboard</h2>
|
||||
<div class="content-header-actions">
|
||||
<span class="text-muted" id="header-date"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Compliance Score + Stats -->
|
||||
<div class="grid-3 mb-24" style="grid-template-columns: 1fr 2fr;">
|
||||
<!-- Gauge -->
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div id="compliance-gauge">
|
||||
<div class="spinner-lg" style="margin:40px auto;"></div>
|
||||
</div>
|
||||
<p class="text-muted mt-8" style="font-size:0.8rem;">Punteggio complessivo di conformita' NIS2</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div>
|
||||
<div class="stats-grid" id="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon danger">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<h4>Rischi Aperti</h4>
|
||||
<div class="stat-card-value" id="stat-risks">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon warning">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/></svg>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<h4>Incidenti Attivi</h4>
|
||||
<div class="stat-card-value" id="stat-incidents">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<h4>Policy Approvate</h4>
|
||||
<div class="stat-card-value" id="stat-policies">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0z"/></svg>
|
||||
</div>
|
||||
<div class="stat-card-content">
|
||||
<h4>Formazione</h4>
|
||||
<div class="stat-card-value" id="stat-training">--%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mb-24">
|
||||
<div class="card-header">
|
||||
<h3>Azioni Rapide</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action-btn" onclick="window.location.href='assessment.html'">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
||||
Nuovo Assessment
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="window.location.href='incidents.html'">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6z"/></svg>
|
||||
Registra Incidente
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="generateAIPolicy()">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><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>
|
||||
Genera Policy AI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deadlines + Activity -->
|
||||
<div class="grid-2">
|
||||
<!-- Scadenze -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Prossime Scadenze</h3>
|
||||
</div>
|
||||
<div class="card-body" id="deadlines-list">
|
||||
<div class="spinner" style="margin:20px auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attivita' Recenti -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Attivita' Recenti</h3>
|
||||
</div>
|
||||
<div class="card-body" id="activity-list">
|
||||
<div class="spinner" style="margin:20px auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────
|
||||
loadSidebar();
|
||||
|
||||
// Data corrente nell'header
|
||||
document.getElementById('header-date').textContent = new Date().toLocaleDateString('it-IT', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
|
||||
});
|
||||
|
||||
// ── Caricamento dati ─────────────────────────────────────
|
||||
loadDashboard();
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const result = await api.getDashboardOverview();
|
||||
|
||||
if (result.success && result.data) {
|
||||
const data = result.data;
|
||||
|
||||
// Compliance gauge
|
||||
const score = data.compliance_score != null ? data.compliance_score : 0;
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
||||
|
||||
// Stats
|
||||
document.getElementById('stat-risks').textContent = data.open_risks != null ? data.open_risks : 0;
|
||||
document.getElementById('stat-incidents').textContent = data.active_incidents != null ? data.active_incidents : 0;
|
||||
document.getElementById('stat-policies').textContent = data.approved_policies != null ? data.approved_policies : 0;
|
||||
document.getElementById('stat-training').textContent = (data.training_completion != null ? data.training_completion : 0) + '%';
|
||||
|
||||
// Scadenze
|
||||
renderDeadlines(data.upcoming_deadlines || []);
|
||||
|
||||
// Attivita'
|
||||
renderActivity(data.recent_activity || []);
|
||||
} else {
|
||||
// Fallback: prova endpoint singoli
|
||||
loadIndividualData();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Dashboard load error:', err);
|
||||
loadIndividualData();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIndividualData() {
|
||||
// Compliance score
|
||||
try {
|
||||
const scoreRes = await api.getComplianceScore();
|
||||
if (scoreRes.success && scoreRes.data) {
|
||||
const score = scoreRes.data.score || scoreRes.data.compliance_score || 0;
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
||||
} else {
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
|
||||
}
|
||||
|
||||
// Deadlines
|
||||
try {
|
||||
const dlRes = await api.getUpcomingDeadlines();
|
||||
renderDeadlines(dlRes.success ? (dlRes.data || []) : []);
|
||||
} catch (e) {
|
||||
renderDeadlines([]);
|
||||
}
|
||||
|
||||
// Activity
|
||||
try {
|
||||
const actRes = await api.getRecentActivity();
|
||||
renderActivity(actRes.success ? (actRes.data || []) : []);
|
||||
} catch (e) {
|
||||
renderActivity([]);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeadlines(deadlines) {
|
||||
const container = document.getElementById('deadlines-list');
|
||||
|
||||
if (!deadlines || deadlines.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>
|
||||
<h4>Nessuna scadenza imminente</h4>
|
||||
<p>Le prossime scadenze appariranno qui.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const months = ['GEN','FEB','MAR','APR','MAG','GIU','LUG','AGO','SET','OTT','NOV','DIC'];
|
||||
let html = '';
|
||||
|
||||
deadlines.slice(0, 5).forEach(dl => {
|
||||
const d = new Date(dl.due_date || dl.date);
|
||||
const now = new Date();
|
||||
const diffDays = Math.ceil((d - now) / 86400000);
|
||||
let urgency = 'normal';
|
||||
if (diffDays <= 3) urgency = 'urgent';
|
||||
else if (diffDays <= 7) urgency = 'soon';
|
||||
|
||||
html += `
|
||||
<div class="deadline-item">
|
||||
<div class="deadline-date ${urgency}">
|
||||
<span class="day">${d.getDate()}</span>
|
||||
<span class="month">${months[d.getMonth()]}</span>
|
||||
</div>
|
||||
<div class="deadline-info">
|
||||
<div class="deadline-title">${escapeHtml(dl.title || dl.description || 'Scadenza')}</div>
|
||||
<div class="deadline-desc">${escapeHtml(dl.category || dl.type || '')}</div>
|
||||
</div>
|
||||
${diffDays <= 3 ? '<span class="badge badge-danger">Urgente</span>' : diffDays <= 7 ? '<span class="badge badge-warning">Prossima</span>' : ''}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderActivity(activities) {
|
||||
const container = document.getElementById('activity-list');
|
||||
|
||||
if (!activities || activities.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
||||
<h4>Nessuna attivita' recente</h4>
|
||||
<p>Le attivita' appariranno qui quando inizierai ad usare la piattaforma.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<ul class="activity-list">';
|
||||
|
||||
activities.slice(0, 8).forEach(act => {
|
||||
html += `
|
||||
<li class="activity-item">
|
||||
<div class="activity-icon">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">${escapeHtml(act.description || act.message || act.action || '')}</div>
|
||||
<div class="activity-time">${timeAgo(act.created_at || act.timestamp || act.date)}</div>
|
||||
</div>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += '</ul>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function generateAIPolicy() {
|
||||
showModal('Genera Policy con AI', `
|
||||
<p style="margin-bottom:16px;">Seleziona la categoria per cui generare una policy automatica tramite intelligenza artificiale.</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Categoria Policy</label>
|
||||
<select class="form-select" id="ai-policy-category">
|
||||
<option value="risk_management">Gestione del Rischio</option>
|
||||
<option value="incident_response">Risposta agli Incidenti</option>
|
||||
<option value="business_continuity">Continuita' Operativa</option>
|
||||
<option value="supply_chain">Sicurezza Supply Chain</option>
|
||||
<option value="access_control">Controllo degli Accessi</option>
|
||||
<option value="encryption">Crittografia</option>
|
||||
<option value="hr_security">Sicurezza del Personale</option>
|
||||
<option value="asset_management">Gestione degli Asset</option>
|
||||
</select>
|
||||
</div>
|
||||
`, {
|
||||
footer: `
|
||||
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
||||
<button class="btn btn-primary" onclick="doGeneratePolicy()">Genera</button>
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
async function doGeneratePolicy() {
|
||||
const category = document.getElementById('ai-policy-category').value;
|
||||
closeModal();
|
||||
showNotification('Generazione policy in corso...', 'info');
|
||||
|
||||
try {
|
||||
const result = await api.aiGeneratePolicy(category);
|
||||
if (result.success) {
|
||||
showNotification('Policy generata con successo!', 'success');
|
||||
setTimeout(() => window.location.href = 'policies.html', 1500);
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nella generazione.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Front Controller / Router
|
||||
*
|
||||
* Tutte le richieste API passano da qui.
|
||||
* URL Pattern: /nis2/api/{controller}/{action}/{id?}
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BOOTSTRAP
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
require_once __DIR__ . '/../application/config/config.php';
|
||||
require_once __DIR__ . '/../application/config/database.php';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CORS HEADERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
|
||||
if (in_array($origin, CORS_ALLOWED_ORIGINS)) {
|
||||
header("Access-Control-Allow-Origin: {$origin}");
|
||||
} elseif (APP_DEBUG) {
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
}
|
||||
|
||||
header("Access-Control-Allow-Methods: " . CORS_ALLOWED_METHODS);
|
||||
header("Access-Control-Allow-Headers: " . CORS_ALLOWED_HEADERS);
|
||||
header("Access-Control-Max-Age: " . CORS_MAX_AGE);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ROUTING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$basePath = '/nis2';
|
||||
|
||||
$path = parse_url($requestUri, PHP_URL_PATH);
|
||||
$path = preg_replace("#^{$basePath}#", '', $path);
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Se non è una richiesta API, servi file statici o index.html
|
||||
if (!preg_match('#^api/#', $path)) {
|
||||
$staticFile = __DIR__ . '/' . $path;
|
||||
if ($path && file_exists($staticFile) && is_file($staticFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($path) || $path === 'index.html') {
|
||||
include __DIR__ . '/index.html';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prova a servire come HTML page
|
||||
$htmlFile = __DIR__ . '/' . $path;
|
||||
if (!str_ends_with($path, '.html')) {
|
||||
$htmlFile = __DIR__ . '/' . $path . '.html';
|
||||
}
|
||||
if (file_exists($htmlFile) && is_file($htmlFile)) {
|
||||
include $htmlFile;
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Not Found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// API ROUTING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$parts = explode('/', $path);
|
||||
array_shift($parts); // Rimuovi "api"
|
||||
|
||||
$controllerName = $parts[0] ?? 'index';
|
||||
$actionName = $parts[1] ?? 'index';
|
||||
$resourceId = isset($parts[2]) ? $parts[2] : null;
|
||||
$subAction = $parts[3] ?? null;
|
||||
$subResourceId = isset($parts[4]) ? $parts[4] : null;
|
||||
|
||||
// Mappa controller
|
||||
$controllerMap = [
|
||||
'auth' => 'AuthController',
|
||||
'organizations' => 'OrganizationController',
|
||||
'assessments' => 'AssessmentController',
|
||||
'dashboard' => 'DashboardController',
|
||||
'risks' => 'RiskController',
|
||||
'incidents' => 'IncidentController',
|
||||
'policies' => 'PolicyController',
|
||||
'supply-chain' => 'SupplyChainController',
|
||||
'training' => 'TrainingController',
|
||||
'assets' => 'AssetController',
|
||||
'audit' => 'AuditController',
|
||||
'admin' => 'AdminController',
|
||||
];
|
||||
|
||||
if (!isset($controllerMap[$controllerName])) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Endpoint non trovato',
|
||||
'error_code' => 'NOT_FOUND',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$controllerClass = $controllerMap[$controllerName];
|
||||
$controllerFile = APP_PATH . "/controllers/{$controllerClass}.php";
|
||||
|
||||
if (!file_exists($controllerFile)) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Controller non disponibile',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once $controllerFile;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// MAPPA AZIONI PER METODO HTTP
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Converti action name con trattini in camelCase
|
||||
$actionName = str_replace('-', '', lcfirst(ucwords($actionName, '-')));
|
||||
|
||||
$actionMap = [
|
||||
// ── AuthController ──────────────────────────────
|
||||
'auth' => [
|
||||
'POST:register' => 'register',
|
||||
'POST:login' => 'login',
|
||||
'POST:logout' => 'logout',
|
||||
'POST:refresh' => 'refresh',
|
||||
'GET:me' => 'me',
|
||||
'PUT:profile' => 'updateProfile',
|
||||
'POST:changePassword' => 'changePassword',
|
||||
],
|
||||
|
||||
// ── OrganizationController ──────────────────────
|
||||
'organizations' => [
|
||||
'POST:create' => 'create',
|
||||
'GET:current' => 'getCurrent',
|
||||
'GET:list' => 'list',
|
||||
'PUT:{id}' => 'update',
|
||||
'GET:{id}/members' => 'listMembers',
|
||||
'POST:{id}/invite' => 'inviteMember',
|
||||
'DELETE:{id}/members/{subId}' => 'removeMember',
|
||||
'POST:classify' => 'classifyEntity',
|
||||
],
|
||||
|
||||
// ── AssessmentController ────────────────────────
|
||||
'assessments' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'GET:{id}/questions' => 'getQuestions',
|
||||
'POST:{id}/respond' => 'saveResponse',
|
||||
'POST:{id}/complete' => 'complete',
|
||||
'GET:{id}/report' => 'getReport',
|
||||
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
||||
],
|
||||
|
||||
// ── DashboardController ─────────────────────────
|
||||
'dashboard' => [
|
||||
'GET:overview' => 'overview',
|
||||
'GET:complianceScore' => 'complianceScore',
|
||||
'GET:upcomingDeadlines' => 'deadlines',
|
||||
'GET:recentActivity' => 'recentActivity',
|
||||
'GET:riskHeatmap' => 'riskHeatmap',
|
||||
],
|
||||
|
||||
// ── RiskController ──────────────────────────────
|
||||
'risks' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
'POST:{id}/treatments' => 'addTreatment',
|
||||
'PUT:treatments/{subId}' => 'updateTreatment',
|
||||
'GET:matrix' => 'getRiskMatrix',
|
||||
'POST:aiSuggest' => 'aiSuggestRisks',
|
||||
],
|
||||
|
||||
// ── IncidentController ──────────────────────────
|
||||
'incidents' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'POST:{id}/timeline' => 'addTimelineEvent',
|
||||
'POST:{id}/earlyWarning' => 'sendEarlyWarning',
|
||||
'POST:{id}/notification' => 'sendNotification',
|
||||
'POST:{id}/finalReport' => 'sendFinalReport',
|
||||
'POST:{id}/aiClassify' => 'aiClassify',
|
||||
],
|
||||
|
||||
// ── PolicyController ────────────────────────────
|
||||
'policies' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
'POST:{id}/approve' => 'approve',
|
||||
'POST:aiGenerate' => 'aiGeneratePolicy',
|
||||
'GET:templates' => 'getTemplates',
|
||||
],
|
||||
|
||||
// ── SupplyChainController ───────────────────────
|
||||
'supply-chain' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
'POST:{id}/assess' => 'assessSupplier',
|
||||
'GET:riskOverview' => 'riskOverview',
|
||||
],
|
||||
|
||||
// ── TrainingController ──────────────────────────
|
||||
'training' => [
|
||||
'GET:courses' => 'listCourses',
|
||||
'POST:courses' => 'createCourse',
|
||||
'GET:assignments' => 'myAssignments',
|
||||
'POST:assign' => 'assignCourse',
|
||||
'PUT:assignments/{subId}' => 'updateAssignment',
|
||||
'GET:complianceStatus' => 'complianceStatus',
|
||||
],
|
||||
|
||||
// ── AssetController ─────────────────────────────
|
||||
'assets' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
'GET:dependencyMap' => 'dependencyMap',
|
||||
],
|
||||
|
||||
// ── AuditController ─────────────────────────────
|
||||
'audit' => [
|
||||
'GET:controls' => 'listControls',
|
||||
'PUT:controls/{subId}' => 'updateControl',
|
||||
'POST:evidence/upload' => 'uploadEvidence',
|
||||
'GET:evidence/list' => 'listEvidence',
|
||||
'GET:report' => 'generateReport',
|
||||
'GET:logs' => 'getAuditLogs',
|
||||
'GET:iso27001Mapping' => 'getIsoMapping',
|
||||
],
|
||||
|
||||
// ── AdminController ─────────────────────────────
|
||||
'admin' => [
|
||||
'GET:organizations' => 'listOrganizations',
|
||||
'GET:users' => 'listUsers',
|
||||
'GET:stats' => 'platformStats',
|
||||
],
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// RISOLUZIONE AZIONE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$actions = $actionMap[$controllerName] ?? [];
|
||||
$resolvedAction = null;
|
||||
|
||||
// Costruisci combinazioni di pattern da verificare (ordine di specificità)
|
||||
$patterns = [];
|
||||
|
||||
if ($subResourceId !== null && $subAction !== null) {
|
||||
// METHOD:action/{id}/subAction/{subId}
|
||||
$patterns[] = "{$method}:{$actionName}/{$subAction}/{subId}";
|
||||
// METHOD:{id}/subAction/{subId}
|
||||
$patterns[] = "{$method}:{id}/{$subAction}/{subId}";
|
||||
}
|
||||
|
||||
if ($subAction !== null && $resourceId !== null) {
|
||||
// METHOD:{id}/subAction
|
||||
$patterns[] = "{$method}:{id}/{$subAction}";
|
||||
// METHOD:action/{subId}
|
||||
$patterns[] = "{$method}:{$actionName}/{subId}";
|
||||
}
|
||||
|
||||
if ($resourceId !== null && $subAction === null) {
|
||||
// METHOD:action/{id} (actionName è in realtà l'ID numerico)
|
||||
if (is_numeric($actionName)) {
|
||||
$patterns[] = "{$method}:{id}";
|
||||
$resourceId = (int) $actionName;
|
||||
} else {
|
||||
// METHOD:{id}
|
||||
$patterns[] = "{$method}:{id}";
|
||||
}
|
||||
}
|
||||
|
||||
// METHOD:action/subAction
|
||||
if ($resourceId !== null && !is_numeric($actionName)) {
|
||||
$patterns[] = "{$method}:{$actionName}/{$resourceId}";
|
||||
}
|
||||
|
||||
// METHOD:action
|
||||
$patterns[] = "{$method}:{$actionName}";
|
||||
|
||||
// Cerca match
|
||||
foreach ($patterns as $pattern) {
|
||||
if (isset($actions[$pattern])) {
|
||||
$resolvedAction = $actions[$pattern];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Se il primo segmento è numerico, l'azione è basata sull'ID
|
||||
if (!$resolvedAction && is_numeric($actionName)) {
|
||||
$resourceId = (int) $actionName;
|
||||
$pattern = "{$method}:{id}";
|
||||
if (isset($actions[$pattern])) {
|
||||
$resolvedAction = $actions[$pattern];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$resolvedAction) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Azione non trovata: {$method} /{$controllerName}/{$actionName}",
|
||||
'error_code' => 'ACTION_NOT_FOUND',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ESECUZIONE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
try {
|
||||
$controller = new $controllerClass();
|
||||
|
||||
if (!method_exists($controller, $resolvedAction)) {
|
||||
http_response_code(501);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Metodo '{$resolvedAction}' non implementato",
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Chiama con gli argomenti appropriati
|
||||
if ($subResourceId !== null) {
|
||||
$controller->$resolvedAction((int) $resourceId, (int) $subResourceId);
|
||||
} elseif ($resourceId !== null && !is_numeric($actionName)) {
|
||||
$controller->$resolvedAction((int) $resourceId);
|
||||
} elseif (is_numeric($actionName)) {
|
||||
$controller->$resolvedAction((int) $resourceId);
|
||||
} else {
|
||||
$controller->$resolvedAction();
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('[DB_ERROR] ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => APP_DEBUG ? 'Errore database: ' . $e->getMessage() : 'Errore interno del server',
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[ERROR] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => APP_DEBUG ? $e->getMessage() : 'Errore interno del server',
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* NIS2 Agile - API Client
|
||||
*
|
||||
* Client JavaScript per comunicare con il backend REST API.
|
||||
*/
|
||||
|
||||
class NIS2API {
|
||||
constructor(baseUrl = '/nis2/api') {
|
||||
this.baseUrl = baseUrl;
|
||||
this.token = localStorage.getItem('nis2_access_token');
|
||||
this.refreshToken = localStorage.getItem('nis2_refresh_token');
|
||||
this.orgId = localStorage.getItem('nis2_org_id');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HTTP Methods
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
async request(method, endpoint, data = null, options = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
if (this.orgId) {
|
||||
headers['X-Organization-Id'] = this.orgId;
|
||||
}
|
||||
|
||||
const config = { method, headers };
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT')) {
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
const json = await response.json();
|
||||
|
||||
// Token expired - try refresh
|
||||
if (response.status === 401 && this.refreshToken && !options._isRetry) {
|
||||
const refreshed = await this.doRefreshToken();
|
||||
if (refreshed) {
|
||||
return this.request(method, endpoint, data, { ...options, _isRetry: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (!json.success && !options.silent) {
|
||||
console.error(`[API] ${method} ${endpoint}:`, json.message);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (error) {
|
||||
console.error(`[API] Network error: ${method} ${endpoint}`, error);
|
||||
return { success: false, message: 'Errore di connessione al server' };
|
||||
}
|
||||
}
|
||||
|
||||
get(endpoint) { return this.request('GET', endpoint); }
|
||||
post(endpoint, data) { return this.request('POST', endpoint, data); }
|
||||
put(endpoint, data) { return this.request('PUT', endpoint, data); }
|
||||
del(endpoint) { return this.request('DELETE', endpoint); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Auth
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
async login(email, password) {
|
||||
const result = await this.post('/auth/login', { email, password });
|
||||
if (result.success) {
|
||||
this.setTokens(result.data.access_token, result.data.refresh_token);
|
||||
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);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async register(email, password, fullName) {
|
||||
const result = await this.post('/auth/register', { email, password, full_name: fullName });
|
||||
if (result.success) {
|
||||
this.setTokens(result.data.access_token, result.data.refresh_token);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async doRefreshToken() {
|
||||
try {
|
||||
const result = await this.post('/auth/refresh', { refresh_token: this.refreshToken });
|
||||
if (result.success) {
|
||||
this.setTokens(result.data.access_token, result.data.refresh_token);
|
||||
return true;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
this.logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.post('/auth/logout', {}).catch(() => {});
|
||||
this.clearTokens();
|
||||
window.location.href = '/nis2/login.html';
|
||||
}
|
||||
|
||||
getMe() { return this.get('/auth/me'); }
|
||||
|
||||
setTokens(access, refresh) {
|
||||
this.token = access;
|
||||
this.refreshToken = refresh;
|
||||
localStorage.setItem('nis2_access_token', access);
|
||||
localStorage.setItem('nis2_refresh_token', refresh);
|
||||
}
|
||||
|
||||
clearTokens() {
|
||||
this.token = null;
|
||||
this.refreshToken = null;
|
||||
this.orgId = null;
|
||||
localStorage.removeItem('nis2_access_token');
|
||||
localStorage.removeItem('nis2_refresh_token');
|
||||
localStorage.removeItem('nis2_org_id');
|
||||
}
|
||||
|
||||
setOrganization(orgId) {
|
||||
this.orgId = orgId;
|
||||
localStorage.setItem('nis2_org_id', orgId);
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Organizations
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
createOrganization(data) { return this.post('/organizations/create', data); }
|
||||
getCurrentOrg() { return this.get('/organizations/current'); }
|
||||
listOrganizations() { return this.get('/organizations/list'); }
|
||||
updateOrganization(id, data) { return this.put(`/organizations/${id}`, data); }
|
||||
classifyEntity(data) { return this.post('/organizations/classify', data); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Assessments
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listAssessments() { return this.get('/assessments/list'); }
|
||||
createAssessment(data) { return this.post('/assessments/create', data); }
|
||||
getAssessment(id) { return this.get(`/assessments/${id}`); }
|
||||
getAssessmentQuestions(id) { return this.get(`/assessments/${id}/questions`); }
|
||||
saveAssessmentResponse(id, data) { return this.post(`/assessments/${id}/respond`, data); }
|
||||
completeAssessment(id) { return this.post(`/assessments/${id}/complete`, {}); }
|
||||
getAssessmentReport(id) { return this.get(`/assessments/${id}/report`); }
|
||||
aiAnalyzeAssessment(id) { return this.post(`/assessments/${id}/ai-analyze`, {}); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Dashboard
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
getDashboardOverview() { return this.get('/dashboard/overview'); }
|
||||
getComplianceScore() { return this.get('/dashboard/compliance-score'); }
|
||||
getUpcomingDeadlines() { return this.get('/dashboard/upcoming-deadlines'); }
|
||||
getRecentActivity() { return this.get('/dashboard/recent-activity'); }
|
||||
getRiskHeatmap() { return this.get('/dashboard/risk-heatmap'); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Risks
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listRisks(params = {}) { return this.get('/risks/list?' + new URLSearchParams(params)); }
|
||||
createRisk(data) { return this.post('/risks/create', data); }
|
||||
getRisk(id) { return this.get(`/risks/${id}`); }
|
||||
updateRisk(id, data) { return this.put(`/risks/${id}`, data); }
|
||||
deleteRisk(id) { return this.del(`/risks/${id}`); }
|
||||
getRiskMatrix() { return this.get('/risks/matrix'); }
|
||||
aiSuggestRisks() { return this.post('/risks/ai-suggest', {}); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Incidents
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listIncidents(params = {}) { return this.get('/incidents/list?' + new URLSearchParams(params)); }
|
||||
createIncident(data) { return this.post('/incidents/create', data); }
|
||||
getIncident(id) { return this.get(`/incidents/${id}`); }
|
||||
updateIncident(id, data) { return this.put(`/incidents/${id}`, data); }
|
||||
sendEarlyWarning(id) { return this.post(`/incidents/${id}/early-warning`, {}); }
|
||||
sendNotification(id) { return this.post(`/incidents/${id}/notification`, {}); }
|
||||
sendFinalReport(id) { return this.post(`/incidents/${id}/final-report`, {}); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Policies
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listPolicies(params = {}) { return this.get('/policies/list?' + new URLSearchParams(params)); }
|
||||
createPolicy(data) { return this.post('/policies/create', data); }
|
||||
getPolicy(id) { return this.get(`/policies/${id}`); }
|
||||
updatePolicy(id, data) { return this.put(`/policies/${id}`, data); }
|
||||
approvePolicy(id) { return this.post(`/policies/${id}/approve`, {}); }
|
||||
aiGeneratePolicy(category) { return this.post('/policies/ai-generate', { category }); }
|
||||
getPolicyTemplates() { return this.get('/policies/templates'); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Supply Chain
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listSuppliers() { return this.get('/supply-chain/list'); }
|
||||
createSupplier(data) { return this.post('/supply-chain/create', data); }
|
||||
getSupplier(id) { return this.get(`/supply-chain/${id}`); }
|
||||
updateSupplier(id, data) { return this.put(`/supply-chain/${id}`, data); }
|
||||
assessSupplier(id, data) { return this.post(`/supply-chain/${id}/assess`, data); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Training
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listCourses() { return this.get('/training/courses'); }
|
||||
getMyTraining() { return this.get('/training/assignments'); }
|
||||
getTrainingCompliance() { return this.get('/training/compliance-status'); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Assets
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listAssets(params = {}) { return this.get('/assets/list?' + new URLSearchParams(params)); }
|
||||
createAsset(data) { return this.post('/assets/create', data); }
|
||||
getAsset(id) { return this.get(`/assets/${id}`); }
|
||||
updateAsset(id, data) { return this.put(`/assets/${id}`, data); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Audit
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
listControls() { return this.get('/audit/controls'); }
|
||||
updateControl(id, data) { return this.put(`/audit/controls/${id}`, data); }
|
||||
generateComplianceReport() { return this.get('/audit/report'); }
|
||||
getAuditLogs(params = {}) { return this.get('/audit/logs?' + new URLSearchParams(params)); }
|
||||
getIsoMapping() { return this.get('/audit/iso27001-mapping'); }
|
||||
}
|
||||
|
||||
// Singleton globale
|
||||
const api = new NIS2API();
|
||||
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* NIS2 Agile - Common Utilities
|
||||
*
|
||||
* Funzioni condivise tra tutte le pagine del frontend.
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Notifications (Toast)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Mostra una notifica toast.
|
||||
* @param {string} message - Testo del messaggio
|
||||
* @param {string} type - 'success' | 'error' | 'warning' | 'info'
|
||||
* @param {number} duration - Durata in ms (default 4000)
|
||||
*/
|
||||
function showNotification(message, type = 'info', duration = 4000) {
|
||||
let container = document.querySelector('.notification-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'notification-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const icons = {
|
||||
success: '<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>',
|
||||
error: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>',
|
||||
warning: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>',
|
||||
info: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>'
|
||||
};
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.innerHTML = `
|
||||
<span class="notification-icon">${icons[type] || icons.info}</span>
|
||||
<span class="notification-message">${escapeHtml(message)}</span>
|
||||
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
|
||||
container.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.add('fade-out');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Modal
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Mostra un dialogo modale.
|
||||
* @param {string} title - Titolo del modale
|
||||
* @param {string} content - Contenuto HTML del body
|
||||
* @param {object} options - { footer: HTML, size: 'sm'|'md'|'lg' }
|
||||
*/
|
||||
function showModal(title, content, options = {}) {
|
||||
closeModal(); // Chiudi eventuale modale aperto
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.id = 'modal-overlay';
|
||||
|
||||
const sizeClass = options.size === 'lg' ? 'style="max-width:720px"' : options.size === 'sm' ? 'style="max-width:400px"' : '';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="modal" ${sizeClass}>
|
||||
<div class="modal-header">
|
||||
<h3>${escapeHtml(title)}</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">${content}</div>
|
||||
${options.footer ? `<div class="modal-footer">${options.footer}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Chiudi cliccando fuori dal modale
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
|
||||
// Chiudi con ESC
|
||||
document.addEventListener('keydown', _modalEscHandler);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => overlay.remove(), 250);
|
||||
}
|
||||
document.removeEventListener('keydown', _modalEscHandler);
|
||||
}
|
||||
|
||||
function _modalEscHandler(e) {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Date Formatting
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Formatta una data in formato italiano (dd/mm/yyyy).
|
||||
* @param {string|Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDate(date) {
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleDateString('it-IT', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatta data e ora in formato italiano.
|
||||
* @param {string|Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDateTime(date) {
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleDateString('it-IT', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restituisce un'etichetta relativa (es. "2 ore fa", "ieri").
|
||||
* @param {string|Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
function timeAgo(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
const now = new Date();
|
||||
const diffMs = now - d;
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHrs = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMin < 1) return 'Adesso';
|
||||
if (diffMin < 60) return `${diffMin} min fa`;
|
||||
if (diffHrs < 24) return `${diffHrs} ore fa`;
|
||||
if (diffDays === 1) return 'Ieri';
|
||||
if (diffDays < 7) return `${diffDays} giorni fa`;
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Sidebar
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Inietta la sidebar di navigazione nell'elemento #sidebar.
|
||||
*/
|
||||
function loadSidebar() {
|
||||
const container = document.getElementById('sidebar');
|
||||
if (!container) return;
|
||||
|
||||
// Determina la pagina corrente
|
||||
const currentPage = window.location.pathname.split('/').pop() || 'dashboard.html';
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
label: 'Principale',
|
||||
items: [
|
||||
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid() },
|
||||
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck() },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Gestione',
|
||||
items: [
|
||||
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation() },
|
||||
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell() },
|
||||
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText() },
|
||||
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink() },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Operativo',
|
||||
items: [
|
||||
{ name: 'Formazione', href: 'training.html', icon: iconAcademicCap() },
|
||||
{ name: 'Asset', href: 'assets.html', icon: iconServer() },
|
||||
{ name: 'Audit & Report', href: 'audit.html', icon: iconChartBar() },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sistema',
|
||||
items: [
|
||||
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog() },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let navHTML = '';
|
||||
|
||||
// Brand
|
||||
navHTML += `
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/><path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1>NIS2 Agile</h1>
|
||||
<span>Compliance Platform</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Nav sections
|
||||
navHTML += '<nav class="sidebar-nav">';
|
||||
for (const section of navItems) {
|
||||
navHTML += `<div class="sidebar-nav-label">${section.label}</div>`;
|
||||
for (const item of section.items) {
|
||||
const isActive = currentPage === item.href ? 'active' : '';
|
||||
navHTML += `<a href="${item.href}" class="${isActive}">${item.icon}<span>${item.name}</span></a>`;
|
||||
}
|
||||
}
|
||||
navHTML += '</nav>';
|
||||
|
||||
// Footer with user info
|
||||
navHTML += `
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-avatar" id="sidebar-user-avatar">--</div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name" id="sidebar-user-name">Caricamento...</div>
|
||||
<div class="sidebar-user-role" id="sidebar-user-role">Utente</div>
|
||||
</div>
|
||||
<button class="sidebar-logout-btn" onclick="api.logout()" title="Esci">
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 001 1h6a1 1 0 100-2H4V5h5a1 1 0 100-2H3zm11.707 3.293a1 1 0 010 1.414L12.414 10l2.293 2.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z" clip-rule="evenodd"/><path fill-rule="evenodd" d="M16 10a1 1 0 00-1-1H8a1 1 0 100 2h7a1 1 0 001-1z" clip-rule="evenodd"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = navHTML;
|
||||
|
||||
// Carica info utente
|
||||
_loadUserInfo();
|
||||
|
||||
// Mobile toggle
|
||||
_setupMobileToggle();
|
||||
}
|
||||
|
||||
async function _loadUserInfo() {
|
||||
try {
|
||||
const result = await api.getMe();
|
||||
if (result.success && result.data) {
|
||||
const user = result.data;
|
||||
const nameEl = document.getElementById('sidebar-user-name');
|
||||
const avatarEl = document.getElementById('sidebar-user-avatar');
|
||||
const roleEl = document.getElementById('sidebar-user-role');
|
||||
if (nameEl) nameEl.textContent = user.full_name || user.email;
|
||||
if (avatarEl) {
|
||||
const initials = (user.full_name || user.email || '--')
|
||||
.split(' ')
|
||||
.map(w => w[0])
|
||||
.slice(0, 2)
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
avatarEl.textContent = initials;
|
||||
}
|
||||
if (roleEl) roleEl.textContent = user.role === 'admin' ? 'Amministratore' : 'Utente';
|
||||
}
|
||||
} catch (e) {
|
||||
// Silenzioso
|
||||
}
|
||||
}
|
||||
|
||||
function _setupMobileToggle() {
|
||||
// Crea pulsante toggle se non esiste
|
||||
if (!document.querySelector('.sidebar-toggle')) {
|
||||
const toggle = document.createElement('button');
|
||||
toggle.className = 'sidebar-toggle';
|
||||
toggle.innerHTML = '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>';
|
||||
toggle.addEventListener('click', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.classList.toggle('open');
|
||||
});
|
||||
document.body.appendChild(toggle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Auth Check
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Verifica che l'utente sia autenticato. Se non lo e', redirige al login.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkAuth() {
|
||||
if (!api.isAuthenticated()) {
|
||||
window.location.href = 'login.html';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Utilities
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Escape HTML per prevenire XSS.
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce: ritarda l'esecuzione di fn fino a delay ms dopo l'ultima chiamata.
|
||||
* @param {Function} fn
|
||||
* @param {number} delay
|
||||
* @returns {Function}
|
||||
*/
|
||||
function debounce(fn, delay = 300) {
|
||||
let timer;
|
||||
return function (...args) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restituisce la classe CSS per un punteggio di conformita'.
|
||||
* @param {number} score - Valore 0-100
|
||||
* @returns {string}
|
||||
*/
|
||||
function getScoreClass(score) {
|
||||
if (score >= 80) return 'score-excellent';
|
||||
if (score >= 60) return 'score-good';
|
||||
if (score >= 40) return 'score-medium';
|
||||
if (score >= 20) return 'score-low';
|
||||
return 'score-critical';
|
||||
}
|
||||
|
||||
/**
|
||||
* Restituisce il colore esadecimale per un punteggio.
|
||||
* @param {number} score - Valore 0-100
|
||||
* @returns {string}
|
||||
*/
|
||||
function getScoreColor(score) {
|
||||
if (score >= 80) return '#34a853';
|
||||
if (score >= 60) return '#84cc16';
|
||||
if (score >= 40) return '#fbbc04';
|
||||
if (score >= 20) return '#f97316';
|
||||
return '#ea4335';
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea l'SVG per il gauge circolare del punteggio.
|
||||
* @param {number} score - Valore 0-100
|
||||
* @param {number} size - Dimensione in px (default 180)
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
function renderScoreGauge(score, size = 180) {
|
||||
const radius = (size / 2) - 14;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (score / 100) * circumference;
|
||||
const color = getScoreColor(score);
|
||||
const cls = getScoreClass(score);
|
||||
|
||||
return `
|
||||
<div class="score-gauge ${cls}" style="width:${size}px;height:${size}px">
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
||||
<circle class="score-gauge-circle-bg" cx="${size/2}" cy="${size/2}" r="${radius}"/>
|
||||
<circle class="score-gauge-circle" cx="${size/2}" cy="${size/2}" r="${radius}"
|
||||
stroke="${color}"
|
||||
stroke-dasharray="${circumference}"
|
||||
stroke-dashoffset="${offset}"
|
||||
style="--score-color: ${color}"/>
|
||||
</svg>
|
||||
<div class="score-gauge-value">
|
||||
<div class="score-gauge-number">${Math.round(score)}</div>
|
||||
<div class="score-gauge-label">Compliance</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SVG Icons (inline, nessuna dipendenza esterna)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
function iconGrid() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zm0 8a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zm6-6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zm0 8a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>';
|
||||
}
|
||||
|
||||
function iconClipboardCheck() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconShieldExclamation() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconBell() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/></svg>';
|
||||
}
|
||||
|
||||
function iconDocumentText() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconLink() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconAcademicCap() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0z"/></svg>';
|
||||
}
|
||||
|
||||
function iconServer() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm14 1a1 1 0 11-2 0 1 1 0 012 0zM2 13a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2v-2zm14 1a1 1 0 11-2 0 1 1 0 012 0z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconChartBar() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zm6-4a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zm6-3a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/></svg>';
|
||||
}
|
||||
|
||||
function iconCog() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconPlus() {
|
||||
return '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg>';
|
||||
}
|
||||
|
||||
function iconSparkles() {
|
||||
return '<svg 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>';
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Accedi - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
|
||||
<path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
|
||||
</div>
|
||||
<p class="auth-subtitle">Piattaforma di compliance NIS2</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-body">
|
||||
<div class="auth-error" id="login-error"></div>
|
||||
|
||||
<form id="login-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Indirizzo Email</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</label>
|
||||
<input type="password" id="password" name="password" class="form-input"
|
||||
placeholder="La tua password" autocomplete="current-password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-full" id="login-btn">
|
||||
Accedi
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Non hai un account? <a href="register.html">Registrati</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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('login-form');
|
||||
const errorEl = document.getElementById('login-error');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
errorEl.classList.remove('visible');
|
||||
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!email || !password) {
|
||||
errorEl.textContent = 'Inserisci email e password.';
|
||||
errorEl.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Accesso in corso...';
|
||||
|
||||
try {
|
||||
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 {
|
||||
window.location.href = 'setup-org.html';
|
||||
}
|
||||
} else {
|
||||
errorEl.textContent = result.message || 'Credenziali non valide.';
|
||||
errorEl.classList.add('visible');
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = 'Errore di connessione al server.';
|
||||
errorEl.classList.add('visible');
|
||||
} finally {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Accedi';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registrazione - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
|
||||
<path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
|
||||
</div>
|
||||
<p class="auth-subtitle">Crea il tuo account</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-body">
|
||||
<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>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>Hai gia' un account? <a href="login.html">Accedi</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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');
|
||||
|
||||
// ── Password Strength Indicator ──────────────────────────
|
||||
passwordInput.addEventListener('input', () => {
|
||||
const val = passwordInput.value;
|
||||
const strength = calcPasswordStrength(val);
|
||||
updateStrengthUI(strength);
|
||||
});
|
||||
|
||||
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 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]);
|
||||
}
|
||||
}
|
||||
|
||||
textEl.textContent = passwordInput.value.length > 0 ? labels[level] : '';
|
||||
}
|
||||
|
||||
// ── Form Submit ──────────────────────────────────────────
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
errorEl.classList.remove('visible');
|
||||
|
||||
const fullname = document.getElementById('fullname').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
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...';
|
||||
|
||||
try {
|
||||
const result = await api.register(email, password, fullname);
|
||||
|
||||
if (result.success) {
|
||||
showNotification('Account creato con successo!', 'success');
|
||||
// Dopo la registrazione, porta al setup organizzazione
|
||||
setTimeout(() => {
|
||||
window.location.href = 'setup-org.html';
|
||||
}, 500);
|
||||
} else {
|
||||
errorEl.textContent = result.message || 'Errore durante la registrazione.';
|
||||
errorEl.classList.add('visible');
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = 'Errore di connessione al server.';
|
||||
errorEl.classList.add('visible');
|
||||
} finally {
|
||||
registerBtn.disabled = false;
|
||||
registerBtn.textContent = 'Crea Account';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,377 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configura Organizzazione - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2>Configurazione Organizzazione</h2>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<div class="grid-2" style="max-width:960px;">
|
||||
<!-- Form -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Dati Aziendali</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="org-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="company-name">Ragione Sociale <span class="required">*</span></label>
|
||||
<input type="text" id="company-name" class="form-input"
|
||||
placeholder="Es. Acme S.r.l." required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="vat-number">Partita IVA</label>
|
||||
<input type="text" id="vat-number" class="form-input"
|
||||
placeholder="IT12345678901" maxlength="16">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="sector">Settore <span class="required">*</span></label>
|
||||
<select id="sector" class="form-select" required>
|
||||
<option value="">-- Seleziona settore --</option>
|
||||
<optgroup label="Settori ad Alta Criticita' (Allegato I)">
|
||||
<option value="energy_electricity">Energia - Elettricita'</option>
|
||||
<option value="energy_district_heating">Energia - Teleriscaldamento</option>
|
||||
<option value="energy_oil">Energia - Petrolio</option>
|
||||
<option value="energy_gas">Energia - Gas</option>
|
||||
<option value="energy_hydrogen">Energia - Idrogeno</option>
|
||||
<option value="transport_air">Trasporti - Aereo</option>
|
||||
<option value="transport_rail">Trasporti - Ferroviario</option>
|
||||
<option value="transport_water">Trasporti - Marittimo/Fluviale</option>
|
||||
<option value="transport_road">Trasporti - Stradale</option>
|
||||
<option value="banking">Banche</option>
|
||||
<option value="financial_markets">Infrastrutture Mercati Finanziari</option>
|
||||
<option value="health">Sanita'</option>
|
||||
<option value="drinking_water">Acqua Potabile</option>
|
||||
<option value="waste_water">Acque Reflue</option>
|
||||
<option value="digital_infrastructure">Infrastruttura Digitale</option>
|
||||
<option value="ict_service_management">Gestione Servizi ICT (B2B)</option>
|
||||
<option value="public_administration">Pubblica Amministrazione</option>
|
||||
<option value="space">Spazio</option>
|
||||
</optgroup>
|
||||
<optgroup label="Altri Settori Critici (Allegato II)">
|
||||
<option value="postal_courier">Servizi Postali e Corrieri</option>
|
||||
<option value="waste_management">Gestione Rifiuti</option>
|
||||
<option value="chemicals">Fabbricazione Prodotti Chimici</option>
|
||||
<option value="food">Produzione e Distribuzione Alimentare</option>
|
||||
<option value="manufacturing_medical">Fabbricazione - Dispositivi Medici</option>
|
||||
<option value="manufacturing_computers">Fabbricazione - Computer/Elettronica</option>
|
||||
<option value="manufacturing_electrical">Fabbricazione - Apparecchiature Elettriche</option>
|
||||
<option value="manufacturing_machinery">Fabbricazione - Macchinari</option>
|
||||
<option value="manufacturing_vehicles">Fabbricazione - Autoveicoli</option>
|
||||
<option value="manufacturing_transport">Fabbricazione - Altri Mezzi di Trasporto</option>
|
||||
<option value="digital_providers">Fornitori Servizi Digitali</option>
|
||||
<option value="research">Organizzazioni di Ricerca</option>
|
||||
</optgroup>
|
||||
<optgroup label="Altro">
|
||||
<option value="other">Altro Settore</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="employee-count">Numero Dipendenti <span class="required">*</span></label>
|
||||
<input type="number" id="employee-count" class="form-input"
|
||||
placeholder="Es. 150" min="1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="annual-turnover">Fatturato Annuo (EUR) <span class="required">*</span></label>
|
||||
<input type="number" id="annual-turnover" class="form-input"
|
||||
placeholder="Es. 15000000" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-full mt-16" id="save-btn">
|
||||
Salva e Continua
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Classification Preview -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Classificazione NIS2</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="classification-preview not-applicable" id="classification-preview">
|
||||
<div class="classification-label">In Attesa</div>
|
||||
<p class="classification-desc">
|
||||
Compila i dati aziendali per ottenere la classificazione automatica
|
||||
secondo i criteri della Direttiva NIS2 (UE) 2022/2555.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-24" id="classification-details" style="display:none;">
|
||||
<h4 style="font-size:0.875rem; margin-bottom:12px; color:var(--gray-700);">Dettagli Classificazione</h4>
|
||||
<div id="classification-info" style="font-size:0.8125rem; color:var(--gray-600); line-height:1.7;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-24">
|
||||
<div class="card-header">
|
||||
<h3>Criteri di Classificazione</h3>
|
||||
</div>
|
||||
<div class="card-body" style="font-size:0.8125rem; color:var(--gray-600); line-height:1.7;">
|
||||
<p><strong>Soggetti Essenziali:</strong></p>
|
||||
<ul style="padding-left:20px; margin-bottom:12px;">
|
||||
<li>Settori Allegato I con ≥ 250 dipendenti o fatturato ≥ 50M EUR</li>
|
||||
<li>Alcuni soggetti designati indipendentemente dalle dimensioni</li>
|
||||
</ul>
|
||||
<p><strong>Soggetti Importanti:</strong></p>
|
||||
<ul style="padding-left:20px; margin-bottom:12px;">
|
||||
<li>Settori Allegato I/II con ≥ 50 dipendenti o fatturato ≥ 10M EUR</li>
|
||||
<li>Non qualificati come essenziali</li>
|
||||
</ul>
|
||||
<p><strong>Non Applicabile:</strong></p>
|
||||
<ul style="padding-left:20px;">
|
||||
<li>Organizzazioni sotto le soglie minime</li>
|
||||
<li>Settori non coperti dalla direttiva</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
// ── Auto-classify on input change ────────────────────────
|
||||
const sectorSelect = document.getElementById('sector');
|
||||
const employeeInput = document.getElementById('employee-count');
|
||||
const turnoverInput = document.getElementById('annual-turnover');
|
||||
|
||||
const debouncedClassify = debounce(autoClassify, 500);
|
||||
|
||||
sectorSelect.addEventListener('change', debouncedClassify);
|
||||
employeeInput.addEventListener('input', debouncedClassify);
|
||||
turnoverInput.addEventListener('input', debouncedClassify);
|
||||
|
||||
async function autoClassify() {
|
||||
const sector = sectorSelect.value;
|
||||
const employees = parseInt(employeeInput.value) || 0;
|
||||
const turnover = parseInt(turnoverInput.value) || 0;
|
||||
|
||||
if (!sector || employees <= 0) {
|
||||
resetClassification();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.classifyEntity({
|
||||
sector: sector,
|
||||
employee_count: employees,
|
||||
annual_turnover: turnover
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
updateClassificationUI(result.data);
|
||||
} else {
|
||||
// Fallback: classificazione locale
|
||||
const localResult = classifyLocally(sector, employees, turnover);
|
||||
updateClassificationUI(localResult);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: classificazione locale
|
||||
const localResult = classifyLocally(sector, employees, turnover);
|
||||
updateClassificationUI(localResult);
|
||||
}
|
||||
}
|
||||
|
||||
function classifyLocally(sector, employees, turnover) {
|
||||
// Settori Allegato I (alta criticita')
|
||||
const annexI = [
|
||||
'energy_electricity', 'energy_district_heating', 'energy_oil', 'energy_gas',
|
||||
'energy_hydrogen', 'transport_air', 'transport_rail', 'transport_water',
|
||||
'transport_road', 'banking', 'financial_markets', 'health', 'drinking_water',
|
||||
'waste_water', 'digital_infrastructure', 'ict_service_management',
|
||||
'public_administration', 'space'
|
||||
];
|
||||
|
||||
// Settori Allegato II
|
||||
const annexII = [
|
||||
'postal_courier', 'waste_management', 'chemicals', 'food',
|
||||
'manufacturing_medical', 'manufacturing_computers', 'manufacturing_electrical',
|
||||
'manufacturing_machinery', 'manufacturing_vehicles', 'manufacturing_transport',
|
||||
'digital_providers', 'research'
|
||||
];
|
||||
|
||||
const isAnnexI = annexI.includes(sector);
|
||||
const isAnnexII = annexII.includes(sector);
|
||||
const isLarge = employees >= 250 || turnover >= 50000000;
|
||||
const isMedium = employees >= 50 || turnover >= 10000000;
|
||||
|
||||
if (isAnnexI && isLarge) {
|
||||
return {
|
||||
classification: 'essential',
|
||||
label: 'Soggetto Essenziale',
|
||||
description: 'La vostra organizzazione rientra tra i soggetti essenziali ai sensi della Direttiva NIS2, in quanto opera in un settore ad alta criticita\' (Allegato I) e supera le soglie dimensionali per la classificazione come grande impresa.'
|
||||
};
|
||||
} else if ((isAnnexI || isAnnexII) && isMedium) {
|
||||
return {
|
||||
classification: 'important',
|
||||
label: 'Soggetto Importante',
|
||||
description: 'La vostra organizzazione rientra tra i soggetti importanti ai sensi della Direttiva NIS2. Siete tenuti a rispettare gli obblighi di sicurezza e di notifica degli incidenti, con un regime di vigilanza ex post.'
|
||||
};
|
||||
} else if (sector === 'other' || (!isAnnexI && !isAnnexII)) {
|
||||
return {
|
||||
classification: 'not_applicable',
|
||||
label: 'Non Applicabile',
|
||||
description: 'In base ai dati forniti, la vostra organizzazione non sembra rientrare nell\'ambito di applicazione della Direttiva NIS2. Consigliamo comunque di verificare con le autorita\' competenti.'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
classification: 'not_applicable',
|
||||
label: 'Sotto le Soglie',
|
||||
description: 'La vostra organizzazione opera in un settore coperto dalla NIS2 ma non raggiunge le soglie dimensionali minime (50 dipendenti o 10M EUR di fatturato). Potreste comunque essere designati dalle autorita\' nazionali.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateClassificationUI(data) {
|
||||
const preview = document.getElementById('classification-preview');
|
||||
const details = document.getElementById('classification-details');
|
||||
const info = document.getElementById('classification-info');
|
||||
|
||||
const classification = data.classification || data.type || 'not_applicable';
|
||||
const label = data.label || classification;
|
||||
const description = data.description || data.explanation || '';
|
||||
|
||||
// Reset classes
|
||||
preview.className = 'classification-preview';
|
||||
|
||||
if (classification === 'essential' || classification === 'essenziale') {
|
||||
preview.classList.add('essential');
|
||||
preview.innerHTML = `
|
||||
<div class="classification-label">Soggetto Essenziale</div>
|
||||
<p class="classification-desc">${escapeHtml(description) || 'Obblighi completi NIS2 - Vigilanza ex ante.'}</p>
|
||||
`;
|
||||
} else if (classification === 'important' || classification === 'importante') {
|
||||
preview.classList.add('important');
|
||||
preview.innerHTML = `
|
||||
<div class="classification-label">Soggetto Importante</div>
|
||||
<p class="classification-desc">${escapeHtml(description) || 'Obblighi NIS2 - Vigilanza ex post.'}</p>
|
||||
`;
|
||||
} else {
|
||||
preview.classList.add('not-applicable');
|
||||
preview.innerHTML = `
|
||||
<div class="classification-label">${escapeHtml(label)}</div>
|
||||
<p class="classification-desc">${escapeHtml(description) || 'Non rientra nell\'ambito NIS2.'}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (data.details || data.obligations) {
|
||||
details.style.display = 'block';
|
||||
let detailsHtml = '';
|
||||
if (data.obligations && data.obligations.length > 0) {
|
||||
detailsHtml += '<p><strong>Obblighi principali:</strong></p><ul style="padding-left:20px;">';
|
||||
data.obligations.forEach(o => {
|
||||
detailsHtml += `<li>${escapeHtml(o)}</li>`;
|
||||
});
|
||||
detailsHtml += '</ul>';
|
||||
}
|
||||
if (data.details) {
|
||||
detailsHtml += `<p>${escapeHtml(data.details)}</p>`;
|
||||
}
|
||||
info.innerHTML = detailsHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function resetClassification() {
|
||||
const preview = document.getElementById('classification-preview');
|
||||
preview.className = 'classification-preview not-applicable';
|
||||
preview.innerHTML = `
|
||||
<div class="classification-label">In Attesa</div>
|
||||
<p class="classification-desc">
|
||||
Compila i dati aziendali per ottenere la classificazione automatica
|
||||
secondo i criteri della Direttiva NIS2 (UE) 2022/2555.
|
||||
</p>
|
||||
`;
|
||||
document.getElementById('classification-details').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Form Submit ──────────────────────────────────────────
|
||||
document.getElementById('org-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const companyName = document.getElementById('company-name').value.trim();
|
||||
const vatNumber = document.getElementById('vat-number').value.trim();
|
||||
const sector = sectorSelect.value;
|
||||
const employees = parseInt(employeeInput.value) || 0;
|
||||
const turnover = parseInt(turnoverInput.value) || 0;
|
||||
|
||||
// Validazione
|
||||
if (!companyName) {
|
||||
showNotification('Inserisci la ragione sociale.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!sector) {
|
||||
showNotification('Seleziona un settore.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (employees <= 0) {
|
||||
showNotification('Inserisci il numero di dipendenti.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (turnover <= 0) {
|
||||
showNotification('Inserisci il fatturato annuo.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Salvataggio in corso...';
|
||||
|
||||
try {
|
||||
const result = await api.createOrganization({
|
||||
name: companyName,
|
||||
vat_number: vatNumber,
|
||||
sector: sector,
|
||||
employee_count: employees,
|
||||
annual_turnover: turnover
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
// Imposta l'organizzazione come attiva
|
||||
api.setOrganization(result.data.id || result.data.organization_id);
|
||||
showNotification('Organizzazione creata con successo!', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = 'dashboard.html';
|
||||
}, 1000);
|
||||
} else {
|
||||
showNotification(result.message || 'Errore nella creazione.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showNotification('Errore di connessione al server.', 'error');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Salva e Continua';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user