nis2-agile/public/assessment.html
Cristiano Benassati ae78a2f7f4 [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>
2026-02-17 17:50:18 +01:00

586 lines
28 KiB
HTML

<!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>