nis2-agile/public/assessment.html
DevEnv nis2-agile 782389849f [SEC+UX] Hardening sicurezza + miglioramenti UX pre-audit
SICUREZZA:
- index.php: rimosso CORS wildcard in debug mode (solo origini autorizzate)
- AuthController: getClientIP() con X-Forwarded-For sicuro (proxy-aware)
- AuthController: refresh token con SELECT FOR UPDATE in transazione atomica
- AIService: anonimizzazione dati org nei prompt Anthropic API (no nome/fatturato)

UX AUDIT-READY:
- dashboard.html: gauge rinominato 'Avanzamento implementazione misure Art.21'
- incidents.html: decision tree Art.23 con 5 criteri per 'Is Significant?'
- policies.html: banner warning obbligatorio su bozze generate da AI
- risks.html: tooltip dettagliati scala Likelihood/Impact (ISO 27005)
- assessment.html: progress bar % completamento risposta domande

DB:
- migration 006: indici performance + audit_log immutabile (trigger) + soft delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:01:33 +01:00

698 lines
36 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 data-i18n="assessment.title">Gap Analysis NIS2</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>
<!-- Progress Bar -->
<div style="margin-bottom:16px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<span style="font-size:0.78rem; font-weight:600; color:var(--gray-600);">Avanzamento risposta domande</span>
<span style="font-size:0.78rem; font-weight:700; color:var(--primary);" id="progress-pct">0%</span>
</div>
<div style="background:var(--gray-200); border-radius:99px; height:8px; overflow:hidden;">
<div id="progress-bar-fill" style="height:100%; background:var(--primary); border-radius:99px; transition:width 0.3s ease; width:0%;"></div>
</div>
<div style="font-size:0.7rem; color:var(--gray-400); margin-top:4px;">
<span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
</div>
</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>
<!-- Generate NCR from gaps -->
<div class="card mb-24" id="ncr-generation-card">
<div class="card-header">
<h3>Non Conformita'</h3>
</div>
<div class="card-body" style="text-align:center; padding:32px;">
<p style="color:var(--gray-600); margin-bottom:16px; font-size:0.875rem;">
Genera automaticamente le non conformita' (NCR) dai gap identificati nell'assessment.
I controlli con stato "Non Implementato" saranno classificati come <strong>major</strong>,
quelli "Parzialmente Implementato" come <strong>minor</strong>.
</p>
<button class="btn btn-primary" id="btn-generate-ncr" onclick="generateNCRs()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z" clip-rule="evenodd"/></svg>
Genera Non Conformita' dai Gap
</button>
<div id="ncr-generation-result" style="display:none; margin-top:16px;"></div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<script src="js/i18n.js"></script>
<script src="js/help.js"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
HelpSystem.init();
// ── 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) {
// API returns array of {category_id, category_title, questions: [...]}
const data = result.data;
if (Array.isArray(data) && data.length > 0 && data[0].questions) {
questions = [];
data.forEach(cat => {
(cat.questions || []).forEach(q => questions.push(q));
});
} else {
questions = 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 (backend puts response_value directly on question)
questions.forEach(q => {
if (q.response_value) {
responses[q.id] = {
answer: q.response_value,
maturity: q.maturity_level ? parseInt(q.maturity_level) : 0,
notes: q.notes || '',
evidence: q.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 updateProgressBar() {
const total = questions.length;
const answered = Object.values(responses).filter(r => r && r.answer).length;
const pct = total > 0 ? Math.round((answered / total) * 100) : 0;
const pctEl = document.getElementById('progress-pct');
const fillEl = document.getElementById('progress-bar-fill');
const answeredEl = document.getElementById('progress-answered');
const totalEl = document.getElementById('progress-total');
if (pctEl) pctEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (answeredEl) answeredEl.textContent = answered;
if (totalEl) totalEl.textContent = total;
}
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: 'not_implemented', label: 'Non Implementato', cls: 'danger' },
{ value: 'partial', label: 'Parziale', cls: 'warning' },
{ value: 'implemented', label: 'Implementato', cls: 'success' },
{ value: 'not_applicable', 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.question_text || q.text || q.title || '')}
</p>
${q.guidance_it ? `<p class="text-muted" style="font-size:0.8125rem;">${escapeHtml(q.guidance_it)}</p>` : ''}
${q.nis2_article ? `<span class="tag mt-8">Art. ${escapeHtml(q.nis2_article)}</span>` : ''}
${q.iso27001_control ? `<span class="tag mt-8" style="margin-left:4px;">ISO ${escapeHtml(q.iso27001_control)}</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;
updateProgressBar();
// 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_code: q.question_code,
response_value: 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_code: q.question_code,
response_value: 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');
}
}
// ── Generate NCRs from Assessment Gaps ─────────────────────
async function generateNCRs() {
if (!currentAssessmentId) {
showNotification('Nessun assessment selezionato.', 'warning');
return;
}
const btn = document.getElementById('btn-generate-ncr');
btn.disabled = true;
btn.innerHTML = '<div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0;"></div> Generazione in corso...';
try {
const result = await api.createNCRsFromAssessment(currentAssessmentId);
const resultDiv = document.getElementById('ncr-generation-result');
resultDiv.style.display = 'block';
if (result.success && result.data) {
const total = result.data.total || 0;
if (total > 0) {
const majorCount = (result.data.created || []).filter(n => n.severity === 'major').length;
const minorCount = total - majorCount;
resultDiv.innerHTML = `
<div style="background:var(--secondary-bg); border:1px solid #bbf7d0; border-radius:var(--border-radius); padding:16px; text-align:left;">
<strong style="color:#15803d;">${total} non conformita' generate con successo</strong>
<div style="margin-top:8px; font-size:0.8125rem; color:var(--gray-600);">
${majorCount > 0 ? `<span style="color:var(--danger); font-weight:600;">${majorCount} major</span> ` : ''}
${minorCount > 0 ? `<span style="color:var(--warning); font-weight:600;">${minorCount} minor</span>` : ''}
</div>
</div>`;
showNotification(`${total} non conformita' generate.`, 'success');
} else {
resultDiv.innerHTML = '<p style="color:var(--gray-500);">Nessun gap trovato — tutti i controlli sono implementati.</p>';
showNotification('Nessun gap trovato.', 'info');
}
btn.style.display = 'none';
} else {
resultDiv.innerHTML = `<p style="color:var(--danger);">${escapeHtml(result.message || 'Errore nella generazione.')}</p>`;
showNotification(result.message || 'Errore.', 'error');
btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z" clip-rule="evenodd"/></svg> Genera Non Conformita\' dai Gap';
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z" clip-rule="evenodd"/></svg> Genera Non Conformita\' dai Gap';
}
}
</script>
</body>
</html>