nis2-agile/public/supplier-assessment.html
DevEnv nis2-agile 1a5db30073 [FEAT] Self-assessment fornitori (P3 supply chain) - portale pubblico con token
- Migrazione 027: tabella supplier_questionnaires (token hash, risposte, score, risk_level, scadenza)
- SupplyChainController: sendQuestionnaire (JWT, genera link 30gg), publicQuestionnaire + submitPublicQuestionnaire (NO auth, token), questionnaireStatus
- 8 domande sicurezza Art.21.2.d (ISO27001/MFA/patching/backup/incident/access/encryption/subfornitori) pesate -> score 0-100 -> risk_level + aggiornamento suppliers.risk_score
- public/supplier-assessment.html: portale standalone (no login) per il fornitore
- Pulito route map supplychain (rimossi duplicati + entry malformata PueT + metodi inesistenti)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:19:01 +02:00

119 lines
6.2 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<title>Questionario di Sicurezza Fornitore — NIS2 Agile</title>
<style>
:root { --primary:#06B6D4; --ok:#22c55e; --warn:#eab308; --bad:#ef4444; --ink:#0f172a; --muted:#64748b; --line:#e2e8f0; }
* { box-sizing:border-box; }
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:#f1f5f9; color:var(--ink); line-height:1.5; }
.wrap { max-width:720px; margin:0 auto; padding:24px 16px 60px; }
.brand { display:flex; align-items:center; gap:10px; margin-bottom:8px; }
.brand .dot { width:14px; height:14px; border-radius:4px; background:var(--primary); }
.brand strong { font-size:1.05rem; }
.card { background:#fff; border:1px solid var(--line); border-radius:12px; padding:22px; margin-top:16px; box-shadow:0 1px 3px rgba(0,0,0,.05); }
h1 { font-size:1.4rem; margin:.2rem 0; }
.sub { color:var(--muted); font-size:.9rem; }
.q { padding:14px 0; border-bottom:1px solid var(--line); }
.q:last-child { border-bottom:none; }
.q p { margin:0 0 8px; font-weight:500; }
.opts { display:flex; gap:8px; flex-wrap:wrap; }
.opt { flex:1; min-width:90px; text-align:center; padding:9px 6px; border:1.5px solid var(--line); border-radius:8px; cursor:pointer; font-size:.85rem; transition:.15s; user-select:none; }
.opt:hover { border-color:var(--primary); }
.opt.sel { background:var(--primary); color:#fff; border-color:var(--primary); }
.btn { width:100%; margin-top:18px; padding:13px; border:none; border-radius:8px; background:var(--primary); color:#fff; font-size:1rem; font-weight:600; cursor:pointer; }
.btn:disabled { opacity:.5; cursor:not-allowed; }
.msg { padding:14px; border-radius:8px; margin-top:12px; font-size:.9rem; }
.msg.err { background:#fef2f2; color:var(--bad); }
.msg.ok { background:#f0fdf4; color:var(--ok); }
.result { text-align:center; padding:20px; }
.result .score { font-size:3rem; font-weight:700; color:var(--primary); }
.spinner { width:34px; height:34px; border:3px solid var(--line); border-top-color:var(--primary); border-radius:50%; animation:spin .8s linear infinite; margin:40px auto; }
@keyframes spin { to { transform:rotate(360deg); } }
.foot { text-align:center; color:var(--muted); font-size:.75rem; margin-top:24px; }
</style>
</head>
<body>
<div class="wrap">
<div class="brand"><span class="dot"></span><strong>NIS2 Agile</strong></div>
<div id="root"><div class="spinner"></div></div>
<div class="foot">Questionario di sicurezza ai sensi dell'Art. 21.2 (d) Direttiva (UE) 2022/2555 (NIS2). I dati sono trattati dal committente per la valutazione del rischio della catena di approvvigionamento.</div>
</div>
<script>
const API = '/api';
const token = new URLSearchParams(location.search).get('token') || '';
const root = document.getElementById('root');
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
let QUESTIONS = [], SCALE = {}, answers = {};
async function load() {
if (!token) { return showErr('Link non valido: token mancante.'); }
try {
const r = await fetch(`${API}/supplychain/public-questionnaire?token=${encodeURIComponent(token)}`);
const j = await r.json();
if (!j.success) return showErr(j.message || 'Questionario non disponibile.');
QUESTIONS = j.data.questions; SCALE = j.data.answer_scale;
render(j.data.supplier_name);
} catch (e) { showErr('Errore di connessione.'); }
}
function render(supplierName) {
const qs = QUESTIONS.map((q, i) => `
<div class="q" data-key="${esc(q.key)}">
<p>${i + 1}. ${esc(q.question)}</p>
<div class="opts">
${Object.entries(SCALE).map(([v, lbl]) => `<div class="opt" data-v="${esc(v)}" onclick="pick('${esc(q.key)}','${esc(v)}',this)">${esc(lbl)}</div>`).join('')}
</div>
</div>`).join('');
root.innerHTML = `
<div class="card">
<h1>Questionario di Sicurezza</h1>
<p class="sub">Compilato da: <strong>${esc(supplierName)}</strong></p>
</div>
<div class="card">${qs}
<div id="formMsg"></div>
<button class="btn" id="submitBtn" onclick="submit()">Invia questionario</button>
</div>`;
}
function pick(key, val, el) {
answers[key] = val;
el.parentNode.querySelectorAll('.opt').forEach(o => o.classList.remove('sel'));
el.classList.add('sel');
}
async function submit() {
if (Object.keys(answers).length < QUESTIONS.length) {
document.getElementById('formMsg').innerHTML = '<div class="msg err">Rispondi a tutte le domande prima di inviare.</div>';
return;
}
const btn = document.getElementById('submitBtn');
btn.disabled = true; btn.textContent = 'Invio in corso...';
try {
const r = await fetch(`${API}/supplychain/submit-public-questionnaire`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, answers })
});
const j = await r.json();
if (!j.success) { btn.disabled = false; btn.textContent = 'Invia questionario';
document.getElementById('formMsg').innerHTML = `<div class="msg err">${esc(j.message)}</div>`; return; }
const lvl = { low:['Basso','#22c55e'], medium:['Medio','#eab308'], high:['Alto','#f97316'], critical:['Critico','#ef4444'] }[j.data.risk_level] || ['',''];
root.innerHTML = `<div class="card result">
<div class="score">${j.data.score}/100</div>
<p>Grazie. Il questionario e stato inviato con successo.</p>
<p class="sub">Livello di rischio valutato: <strong style="color:${lvl[1]}">${lvl[0]}</strong></p>
</div>`;
} catch (e) {
btn.disabled = false; btn.textContent = 'Invia questionario';
document.getElementById('formMsg').innerHTML = '<div class="msg err">Errore di connessione.</div>';
}
}
function showErr(m) { root.innerHTML = `<div class="card"><div class="msg err">${esc(m)}</div></div>`; }
load();
</script>
</body>
</html>