nis2-agile/public/supplier-portal.html
DevEnv nis2-agile 78dcb412ad [FEAT] Fase 3+4 frontend: supplier-portal.html (OTP/magic-link + compilazione)
Portale fornitore pubblico self-contained (CSS+JS inline, noindex):
- Auth: richiesta OTP (risposta opaca), verifica codice con errori specifici +
  countdown reinvio 60s, magic-link da ?magic= (consumo su click), sessione
  JWT supplier in sessionStorage (4h).
- Dashboard: 1 questionario aperto -> diretto, multi -> lista con badge scadenza.
- Compilazione (Fase 4): tutti i tipi domanda (yes_no_partial, single/multi_choice,
  scale_1_5 con etichette estremi, number, text, file) + "Non applicabile";
  progress bar, badge scadenza sticky, help_text + nis2_ref visibili.
- Autosave PATCH debounce 800ms + "bozza salvata"; salva bozza vs invia definitivo
  con conferma + validazione obbligatorie client.
- Ricevuta: conferma + score solo se show_score. Footer GDPR Art.28.
- a11y: input nativi, OTP autocomplete=one-time-code inputmode=numeric, target >=44px.

Inline JS validato (node --check). File statico -> live via nginx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:42:23 +02:00

442 lines
22 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>Portale Fornitori - NIS2 Agile</title>
<style>
:root{--primary:#06B6D4;--primary-dark:#0891b2;--gray-50:#f8fafc;--gray-100:#f1f5f9;--gray-200:#e2e8f0;--gray-400:#94a3b8;--gray-500:#64748b;--gray-700:#334155;--gray-900:#0f172a;--danger:#dc2626;--warning:#f59e0b;--success:#16a34a;--radius:10px}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--gray-100);color:var(--gray-900);line-height:1.6;min-height:100vh}
.wrap{max-width:560px;margin:0 auto;padding:24px 16px}
.card{background:#fff;border-radius:var(--radius);box-shadow:0 1px 3px rgba(0,0,0,.08);padding:24px;margin-bottom:16px}
.brand{text-align:center;margin-bottom:20px}
.brand h1{font-size:1.15rem;color:var(--primary-dark)}
.brand p{font-size:.85rem;color:var(--gray-500)}
h2{font-size:1.1rem;margin-bottom:12px}
p.sub{color:var(--gray-500);font-size:.9rem;margin-bottom:16px}
label{display:block;font-size:.85rem;font-weight:600;margin-bottom:6px;color:var(--gray-700)}
input[type=email],input[type=text],input[type=number],textarea,select{width:100%;padding:12px;border:1px solid var(--gray-200);border-radius:8px;font-size:1rem;font-family:inherit}
textarea{min-height:88px;resize:vertical}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:48px;padding:12px 20px;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;background:var(--primary);color:#fff;transition:background .15s}
.btn:hover{background:var(--primary-dark)}
.btn:disabled{opacity:.6;cursor:not-allowed}
.btn-ghost{background:transparent;color:var(--primary-dark);min-height:auto;width:auto;padding:6px 0;font-size:.85rem;text-decoration:underline}
.btn-secondary{background:var(--gray-100);color:var(--gray-700)}
.btn-secondary:hover{background:var(--gray-200)}
.msg{padding:12px 14px;border-radius:8px;font-size:.88rem;margin:12px 0}
.msg.err{background:#fef2f2;color:var(--danger);border-left:3px solid var(--danger)}
.msg.ok{background:#f0fdf4;color:var(--success);border-left:3px solid var(--success)}
.msg.info{background:#eff6ff;color:#1e40af;border-left:3px solid #1e40af}
.otp-input{letter-spacing:.4em;text-align:center;font-size:1.4rem;font-family:monospace}
.muted{color:var(--gray-400);font-size:.78rem;margin-top:8px}
.hidden{display:none!important}
.spinner{width:32px;height:32px;border:3px solid var(--gray-200);border-top-color:var(--primary);border-radius:50%;animation:spin .8s linear infinite;margin:30px auto}
@keyframes spin{to{transform:rotate(360deg)}}
.q{padding:16px 0;border-bottom:1px solid var(--gray-100)}
.q:last-child{border-bottom:none}
.q-text{font-size:.95rem;font-weight:600;margin-bottom:4px}
.q-help{font-size:.8rem;color:var(--gray-500);margin-bottom:10px}
.q-ref{display:inline-block;background:#eff6ff;color:var(--primary-dark);font-size:.7rem;font-weight:700;padding:2px 7px;border-radius:20px;margin-left:6px}
.opts{display:flex;flex-wrap:wrap;gap:8px}
.opt{flex:0 0 auto}
.opt input{position:absolute;opacity:0}
.opt label{display:inline-flex;align-items:center;min-height:44px;padding:8px 16px;border:1.5px solid var(--gray-200);border-radius:24px;cursor:pointer;font-weight:600;margin:0;font-size:.9rem}
.opt input:checked+label{background:var(--primary);color:#fff;border-color:var(--primary)}
.scale-ends{display:flex;justify-content:space-between;font-size:.72rem;color:var(--gray-400);margin-top:4px}
.badge{display:inline-block;padding:3px 10px;border-radius:20px;font-size:.72rem;font-weight:700}
.badge-warn{background:#fffbeb;color:#a16207}.badge-danger{background:#fef2f2;color:var(--danger)}.badge-neutral{background:var(--gray-100);color:var(--gray-500)}.badge-ok{background:#f0fdf4;color:var(--success)}
.due-bar{position:sticky;top:0;background:#fff;padding:10px 0;z-index:5;border-bottom:1px solid var(--gray-100);margin-bottom:8px}
.progress{height:6px;background:var(--gray-100);border-radius:3px;overflow:hidden;margin:8px 0}
.progress-fill{height:100%;background:var(--primary);transition:width .3s}
.camp{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:14px 0;border-bottom:1px solid var(--gray-100);flex-wrap:wrap}
.camp:last-child{border-bottom:none}
.save-state{font-size:.78rem;color:var(--gray-400);text-align:right;min-height:18px}
.foot{text-align:center;font-size:.72rem;color:var(--gray-400);margin-top:20px;line-height:1.5}
</style>
</head>
<body>
<div class="wrap">
<div class="brand">
<h1 id="brand-title">Portale Fornitori</h1>
<p id="brand-sub">Compilazione questionari di sicurezza NIS2</p>
</div>
<!-- LOADING -->
<div id="view-loading" class="card"><div class="spinner"></div></div>
<!-- RICHIESTA ACCESSO -->
<div id="view-request" class="card hidden">
<h2>Accedi al portale</h2>
<p class="sub">Inserisci l'email a cui hai ricevuto l'invito. Ti invieremo un link di accesso e un codice.</p>
<div id="req-msg"></div>
<label for="email">Email</label>
<input type="email" id="email" inputmode="email" autocomplete="email" placeholder="nome@azienda.it" autofocus>
<div style="height:12px"></div>
<button class="btn" id="btn-request">Invia link di accesso</button>
<p class="muted">Trattiamo la tua email solo per gestire il questionario. <a href="#" id="privacy-link">Informativa privacy</a>.</p>
</div>
<!-- EMAIL INVIATA / VERIFICA OTP -->
<div id="view-verify" class="card hidden">
<h2>Controlla la tua email</h2>
<p class="sub">Abbiamo inviato un link di accesso e un codice a <strong id="verify-email"></strong>. Apri l'email e tocca il pulsante, oppure inserisci qui sotto il codice.</p>
<div class="msg info">Non la trovi? Controlla la cartella <strong>spam/posta indesiderata</strong>.</div>
<div id="verify-msg"></div>
<label for="otp">Codice di accesso</label>
<input type="text" id="otp" class="otp-input" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9 ]*" maxlength="11" placeholder="········">
<div style="height:12px"></div>
<button class="btn" id="btn-verify">Entra nel portale</button>
<div style="height:10px"></div>
<button class="btn-ghost" id="btn-resend" disabled>Invia di nuovo l'email</button>
<button class="btn-ghost" id="btn-other-email" style="float:right">Usa un'altra email</button>
</div>
<!-- DASHBOARD -->
<div id="view-dash" class="card hidden">
<h2 id="dash-supplier"></h2>
<p class="sub" id="dash-committente"></p>
<div id="dash-campaigns"></div>
<div style="height:14px"></div>
<button class="btn-ghost" id="btn-logout">Esci</button>
</div>
<!-- COMPILAZIONE -->
<div id="view-fill" class="card hidden">
<div class="due-bar">
<button class="btn-ghost" id="btn-back-dash">&larr; Torna ai questionari</button>
<div id="fill-due"></div>
<div class="progress"><div class="progress-fill" id="fill-progress" style="width:0"></div></div>
<div style="font-size:.78rem;color:var(--gray-500)" id="fill-count"></div>
<div class="save-state" id="fill-savestate"></div>
</div>
<h2 id="fill-title">Questionario di sicurezza</h2>
<div id="fill-questions"></div>
<div id="fill-msg"></div>
<div style="height:8px"></div>
<button class="btn" id="btn-submit">Invia definitivo</button>
<div style="height:8px"></div>
<button class="btn btn-secondary" id="btn-draft">Salva bozza</button>
</div>
<!-- COMPLETATO -->
<div id="view-done" class="card hidden">
<div style="text-align:center;font-size:3rem">&#10003;</div>
<h2 style="text-align:center">Questionario inviato. Grazie!</h2>
<p class="sub" style="text-align:center" id="done-msg"></p>
<div id="done-score"></div>
<button class="btn-ghost" id="btn-done-back" style="display:block;margin:0 auto">Torna ai questionari</button>
</div>
<div class="foot">
Servizio fornito da <strong>NIS2 Agile</strong> per conto del committente.<br>
Il committente è titolare del trattamento; NIS2 Agile agisce come responsabile (Art. 28 GDPR).<br>
Se non eri tu a richiedere questo accesso, ignora l'email ricevuta.
</div>
</div>
<script>
'use strict';
const API = '/api/supplier-portal';
let TOKEN = sessionStorage.getItem('sp_token') || '';
let RESEND_TIMER = null;
let CURRENT = null; // {campaignId, questions, answers, dueAt}
let SAVE_DEBOUNCE = null;
function $(id){return document.getElementById(id);}
function show(view){['loading','request','verify','dash','fill','done'].forEach(v=>$('view-'+v).classList.toggle('hidden',v!==view));}
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
function msg(el,text,type){el.innerHTML=text?('<div class="msg '+type+'">'+esc(text)+'</div>'):'';}
async function api(method,path,body,auth){
const h={'Content-Type':'application/json'};
if(auth&&TOKEN)h['Authorization']='Bearer '+TOKEN;
const opt={method,headers:h};
if(body)opt.body=JSON.stringify(body);
let res,json;
try{res=await fetch(API+path,opt);json=await res.json();}
catch(e){return{success:false,message:'Errore di connessione. Riprova.',_net:true};}
json._status=res.status;
return json;
}
// ── Boot: magic-link? token? ──
(async function boot(){
const params=new URLSearchParams(location.search);
const magic=params.get('magic');
if(magic){
show('loading');
const r=await api('POST','/verify-otp',{magic_token:magic},false);
history.replaceState({},'',location.pathname); // pulisci URL
if(r.success&&r.data&&r.data.token){onLogin(r.data);return;}
show('request');
msg($('req-msg'),(r.message||'Link non valido o scaduto. Richiedi un nuovo accesso.'),'err');
return;
}
if(TOKEN){
show('loading');
const r=await api('GET','/me',null,true);
if(r.success&&r.data){renderDash(r.data);return;}
TOKEN='';sessionStorage.removeItem('sp_token');
}
show('request');
})();
// ── Richiesta OTP ──
$('btn-request').onclick=async function(){
const email=$('email').value.trim();
if(!email||email.indexOf('@')<0){msg($('req-msg'),'Inserisci un indirizzo email valido.','err');return;}
this.disabled=true;
const r=await api('POST','/request-otp',{email},false);
this.disabled=false;
// risposta sempre opaca: passiamo comunque alla verifica
$('verify-email').textContent=email;
sessionStorage.setItem('sp_email',email);
show('verify');
startResendCountdown();
$('otp').focus();
};
$('email').addEventListener('keydown',e=>{if(e.key==='Enter')$('btn-request').click();});
function startResendCountdown(){
let s=60;const btn=$('btn-resend');btn.disabled=true;
if(RESEND_TIMER)clearInterval(RESEND_TIMER);
const upd=()=>{btn.textContent=s>0?('Invia di nuovo tra '+s+'s'):'Invia di nuovo l\'email';if(s<=0){btn.disabled=false;clearInterval(RESEND_TIMER);}s--;};
upd();RESEND_TIMER=setInterval(upd,1000);
}
$('btn-resend').onclick=async function(){
const email=sessionStorage.getItem('sp_email')||'';
if(!email)return;
await api('POST','/request-otp',{email},false);
startResendCountdown();
msg($('verify-msg'),'Nuova email inviata.','ok');
};
$('btn-other-email').onclick=()=>{show('request');$('email').focus();};
// ── Verifica OTP ──
$('btn-verify').onclick=async function(){
const email=sessionStorage.getItem('sp_email')||'';
const code=($('otp').value||'').replace(/\D/g,'');
if(!code){msg($('verify-msg'),'Inserisci il codice ricevuto via email.','err');return;}
this.disabled=true;
const r=await api('POST','/verify-otp',{email,code},false);
this.disabled=false;
if(r.success&&r.data&&r.data.token){onLogin(r.data);return;}
// errori specifici con via d'uscita
let m=r.message||'Codice non valido.';
msg($('verify-msg'),m,'err');
$('otp').value='';$('otp').focus();
};
$('otp').addEventListener('keydown',e=>{if(e.key==='Enter')$('btn-verify').click();});
function onLogin(data){
TOKEN=data.token;sessionStorage.setItem('sp_token',TOKEN);
// carica /me per la dashboard completa
api('GET','/me',null,true).then(r=>{if(r.success&&r.data)renderDash(r.data);else{show('request');}});
}
$('btn-logout').onclick=()=>{TOKEN='';sessionStorage.removeItem('sp_token');sessionStorage.removeItem('sp_email');show('request');};
// ── Dashboard ──
function renderDash(data){
const sup=data.supplier||{};const comm=data.committente||{};
$('dash-supplier').textContent=sup.name||'Fornitore';
$('dash-committente').textContent=comm.name?('Questionari richiesti da '+comm.name):'';
$('brand-title').textContent=comm.name||'Portale Fornitori';
const camps=(data.campaigns||[]);
const open=camps.filter(c=>c.status==='sent'||c.status==='in_progress');
// caso 1 solo questionario aperto → vai diretto
if(open.length===1&&camps.length===1){openCampaign(open[0].id);return;}
let html='';
if(camps.length===0){html='<div class="msg info">Nessun questionario in attesa. Se hai ricevuto una richiesta, contatta il tuo referente.</div>';}
camps.forEach(c=>{
const b=campBadge(c);
const cta=(c.status==='completed')?'Vedi ricevuta':(c.status==='expired'?'Scaduto':(c.status==='in_progress'?'Riprendi':'Compila'));
const dis=(c.status==='expired')?'disabled':'';
html+='<div class="camp"><div><div style="font-weight:600">Questionario sicurezza NIS2</div><div style="font-size:.8rem;color:var(--gray-500)">'+b+(c.due_at?(' · scade il '+fmtDate(c.due_at)):'')+'</div></div>'+
'<button class="btn" style="width:auto;min-height:40px;padding:8px 16px" onclick="openCampaign('+c.id+')" '+dis+'>'+cta+'</button></div>';
});
$('dash-campaigns').innerHTML=html;
show('dash');
}
function campBadge(c){
if(c.status==='completed')return '<span class="badge badge-ok">Inviato</span>';
if(c.status==='expired')return '<span class="badge badge-danger">Scaduto</span>';
if(c.status==='in_progress')return '<span class="badge badge-warn">In bozza</span>';
return '<span class="badge badge-neutral">Da compilare</span>';
}
function fmtDate(d){try{return new Date(d).toLocaleDateString('it-IT');}catch(e){return d;}}
// ── Compilazione ──
window.openCampaign=async function(id){
show('loading');
const r=await api('GET','/'+id,null,true);
if(!r.success||!r.data){
if(r._status===410){alert('Il termine per la compilazione è scaduto.');}
const me=await api('GET','/me',null,true);if(me.success)renderDash(me.data);else show('request');return;
}
const d=r.data;
if(d.campaign.status==='completed'){renderDone(d,null);return;}
CURRENT={campaignId:id,questions:d.questions||[],answers:d.answers||{},dueAt:d.campaign.due_at,showScore:d.campaign.show_score};
$('fill-title').textContent='Questionario per '+((d.committente&&d.committente.name)||'il committente');
renderDueBar();
renderQuestions();
msg($('fill-msg'),'','');
show('fill');
};
function renderDueBar(){
const due=CURRENT.dueAt;let html='';
if(due){const days=Math.ceil((new Date(due)-new Date())/86400000);
const cls=days<0?'badge-danger':(days<=7?'badge-warn':'badge-neutral');
html='<span class="badge '+cls+'">'+(days<0?'Scaduto il ':'Scade il ')+fmtDate(due)+'</span>';}
$('fill-due').innerHTML=html;
}
function answerKey(q){return q.id!=null?String(q.id):('code:'+q.code);}
function getAns(q){return CURRENT.answers[answerKey(q)]||{};}
function renderQuestions(){
let html='';
CURRENT.questions.forEach((q,i)=>{
const a=getAns(q);const val=a.value;
html+='<div class="q" data-qi="'+i+'"><div class="q-text">'+esc(q.text)+(q.nis2_ref?'<span class="q-ref">'+esc(q.nis2_ref)+'</span>':'')+(q.is_required?' <span style="color:var(--danger)">*</span>':'')+'</div>';
if(q.help_text)html+='<div class="q-help">'+esc(q.help_text)+'</div>';
html+=renderInput(q,i,val,a.text);
html+='</div>';
});
$('fill-questions').innerHTML=html;
bindInputs();
updateProgress();
}
function renderInput(q,i,val,text){
const name='q'+i;
if(q.type==='yes_no_partial'){
return optGroup(name,[['yes','Sì'],['partial','Parzialmente'],['no','No'],['not_applicable','Non applicabile']],val,'radio');
}
if(q.type==='single_choice'){
return optGroup(name,(q.options||[]).map(o=>[o,o]),val,'radio');
}
if(q.type==='multi_choice'){
const vals=Array.isArray(val)?val:[];
return '<div style="font-size:.75rem;color:var(--gray-400);margin-bottom:6px">Puoi selezionare più opzioni</div>'+optGroup(name,(q.options||[]).map(o=>[o,o]),vals,'checkbox');
}
if(q.type==='scale_1_5'){
const ends=q.options||{};
let h=optGroup(name,[['1','1'],['2','2'],['3','3'],['4','4'],['5','5']],val,'radio');
h+='<div class="scale-ends"><span>1 = '+esc(ends.min||ends['1']||'minimo')+'</span><span>5 = '+esc(ends.max||ends['5']||'massimo')+'</span></div>';
return h;
}
if(q.type==='number'){
return '<input type="number" inputmode="numeric" name="'+name+'" value="'+(val!=null?esc(val):'')+'" data-qi="'+i+'">';
}
if(q.type==='file'){
return '<div class="muted">Formati: PDF, JPG, PNG. <input type="file" name="'+name+'" accept=".pdf,.jpg,.jpeg,.png" data-qi="'+i+'"><div data-fileinfo="'+i+'">'+(text?esc(text):'')+'</div></div>';
}
// text
return '<textarea name="'+name+'" data-qi="'+i+'" placeholder="Scrivi qui...">'+(val!=null?esc(val):'')+'</textarea>';
}
function optGroup(name,opts,val,kind){
const arr=Array.isArray(val)?val.map(String):[String(val)];
let h='<div class="opts">';
opts.forEach((o,j)=>{
const id=name+'_'+j;const checked=arr.indexOf(String(o[0]))>=0?'checked':'';
h+='<div class="opt"><input type="'+kind+'" id="'+id+'" name="'+name+'" value="'+esc(o[0])+'" '+checked+'><label for="'+id+'">'+esc(o[1])+'</label></div>';
});
return h+'</div>';
}
function bindInputs(){
$('fill-questions').querySelectorAll('input,textarea,select').forEach(el=>{
const ev=(el.type==='radio'||el.type==='checkbox')?'change':'input';
el.addEventListener(ev,()=>{collectAndAutosave();});
});
}
function collect(){
const out=[];
CURRENT.questions.forEach((q,i)=>{
const name='q'+i;let value=null,text=null;
if(q.type==='multi_choice'){
value=Array.from($('fill-questions').querySelectorAll('input[name="'+name+'"]:checked')).map(e=>e.value);
if(value.length===0)value=null;
}else if(q.type==='yes_no_partial'||q.type==='single_choice'||q.type==='scale_1_5'){
const sel=$('fill-questions').querySelector('input[name="'+name+'"]:checked');
value=sel?sel.value:null;
}else if(q.type==='number'){
const el=$('fill-questions').querySelector('[name="'+name+'"]');value=el&&el.value!==''?el.value:null;
}else if(q.type==='file'){
const el=$('fill-questions').querySelector('[name="'+name+'"]');text=el&&el.files&&el.files[0]?el.files[0].name:null;
}else{
const el=$('fill-questions').querySelector('[name="'+name+'"]');text=el&&el.value!==''?el.value:null;
}
const entry={};if(q.id!=null)entry.question_id=q.id;else entry.question_code=q.code;
if(value!=null)entry.value=value;if(text!=null)entry.text=text;
// aggiorna cache locale
CURRENT.answers[answerKey(q)]={value:value,text:text};
if(value!=null||text!=null)out.push(entry);
});
return out;
}
function collectAndAutosave(){
updateProgress();
$('fill-savestate').textContent='Salvataggio…';
if(SAVE_DEBOUNCE)clearTimeout(SAVE_DEBOUNCE);
SAVE_DEBOUNCE=setTimeout(async()=>{
const answers=collect();
if(answers.length===0){$('fill-savestate').textContent='';return;}
const r=await api('PATCH','/'+CURRENT.campaignId+'/answers',{answers},true);
$('fill-savestate').textContent=r.success?('Bozza salvata '+new Date().toLocaleTimeString('it-IT').slice(0,5)):'Salvataggio in sospeso';
},800);
}
function updateProgress(){
const total=CURRENT.questions.length;let done=0;
CURRENT.questions.forEach(q=>{const a=getAns(q);if((a.value!=null&&a.value!=='')||(a.text!=null&&a.text!==''))done++;});
$('fill-progress').style.width=(total?Math.round(done/total*100):0)+'%';
$('fill-count').textContent=done+' di '+total+' domande con risposta';
}
$('btn-draft').onclick=async function(){
const answers=collect();
const r=await api('PATCH','/'+CURRENT.campaignId+'/answers',{answers},true);
msg($('fill-msg'),r.success?'Bozza salvata.':'Errore nel salvataggio.',r.success?'ok':'err');
};
$('btn-submit').onclick=async function(){
const answers=collect();
// verifica obbligatorie lato client
const missing=[];
CURRENT.questions.forEach(q=>{if(q.is_required){const a=getAns(q);if(!((a.value!=null&&a.value!=='')||(a.text!=null&&a.text!=='')))missing.push(q);}});
if(missing.length){msg($('fill-msg'),'Mancano '+missing.length+' risposte obbligatorie.','err');return;}
if(!confirm('Confermi l\'invio? Dopo l\'invio non potrai modificare le risposte.'))return;
this.disabled=true;
const r=await api('POST','/'+CURRENT.campaignId+'/submit',{answers},true);
this.disabled=false;
if(r.success){renderDone({committente:{name:$('brand-title').textContent}},r.data);return;}
if(r.data&&r.data.missing){msg($('fill-msg'),'Mancano risposte obbligatorie.','err');return;}
msg($('fill-msg'),r.message||'Errore nell\'invio.','err');
};
$('btn-back-dash').onclick=async function(){const me=await api('GET','/me',null,true);if(me.success)renderDash(me.data);};
function renderDone(d,result){
const comm=(d&&d.committente&&d.committente.name)||'';
$('done-msg').textContent='Abbiamo ricevuto le tue risposte'+(comm?(' e le abbiamo inviate a '+comm):'')+'.';
let s='';
if(result&&result.show_score&&result.score!=null){
s='<div class="msg info" style="text-align:center">Punteggio: <strong>'+result.score+'/100</strong></div>';
}
$('done-score').innerHTML=s;
show('done');
}
$('btn-done-back').onclick=async function(){const me=await api('GET','/me',null,true);if(me.success)renderDash(me.data);else show('request');};
$('privacy-link').onclick=function(e){e.preventDefault();alert('Il committente è titolare del trattamento dei tuoi dati (nome, email, risposte) per la due-diligence di sicurezza NIS2 (Art. 6.1.f/c GDPR). NIS2 Agile agisce come responsabile (Art. 28). Hai diritto di accesso, rettifica e cancellazione. Per esercitarli contatta il committente.');};
</script>
</body>
</html>