[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:
2026-02-17 17:50:18 +01:00
co-authored by Claude Opus 4.6
commit ae78a2f7f4
36 changed files with 10483 additions and 0 deletions
+12
View File
@@ -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]
+29
View File
@@ -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);
+585
View File
@@ -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>
+1641
View File
File diff suppressed because it is too large Load Diff
+324
View File
@@ -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>
+384
View File
@@ -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',
]);
}
+244
View File
@@ -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();
+463
View File
@@ -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()">&times;</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()">&times;</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>';
}
+105
View File
@@ -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>
+179
View File
@@ -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>
+377
View File
@@ -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 &ge; 250 dipendenti o fatturato &ge; 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 &ge; 50 dipendenti o fatturato &ge; 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>