- 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>
119 lines
6.2 KiB
HTML
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 => ({'&':'&','<':'<','>':'>','"':'"'}[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>
|