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>
586 lines
28 KiB
HTML
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>
|