[I18N] Modulo Fornitori bilingue IT/EN (#8b) — v1.24.5

- supplier-portal.html (portale esterno OTP): i18n self-contained (dizionario inline + toggle IT/EN, ~79 stringhe), flusso OTP/token/ID/API invariato; domande questionario restano dati (non tradotte).
- supply-chain.html (interno): i18n estesa (5->25 data-i18n + 97 I18n.t).
- i18n.js: +82 chiavi sp.* (38->120). Cache-buster i18n.js su supply-chain.html. node --check OK, pagine 200, parita IT/EN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-21 10:32:18 +02:00
co-authored by Claude Opus 4.8
parent 14c4c2a7d2
commit 05a1436d8c
4 changed files with 502 additions and 182 deletions
+300 -62
View File
@@ -54,6 +54,11 @@ textarea{min-height:88px;resize:vertical}
.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}
.lang-toggle{display:flex;justify-content:flex-end;gap:0;margin-bottom:8px}
.lang-toggle button{background:transparent;border:1px solid var(--gray-200);color:var(--gray-500);font-size:.78rem;font-weight:700;padding:4px 10px;cursor:pointer;min-height:auto;line-height:1.2}
.lang-toggle button:first-child{border-radius:6px 0 0 6px;border-right:none}
.lang-toggle button:last-child{border-radius:0 6px 6px 0}
.lang-toggle button.active{background:var(--primary);color:#fff;border-color:var(--primary)}
</style>
<!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest">
@@ -71,9 +76,13 @@ textarea{min-height:88px;resize:vertical}
</head>
<body>
<div class="wrap">
<div class="lang-toggle">
<button type="button" id="lang-it" onclick="spSetLang('it')">IT</button>
<button type="button" id="lang-en" onclick="spSetLang('en')">EN</button>
</div>
<div class="brand">
<h1 id="brand-title">Portale Fornitori</h1>
<p id="brand-sub">Compilazione questionari di sicurezza NIS2</p>
<h1 id="brand-title" data-i18n="brand_title">Portale Fornitori</h1>
<p id="brand-sub" data-i18n="brand_sub">Compilazione questionari di sicurezza NIS2</p>
</div>
<!-- LOADING -->
@@ -81,29 +90,29 @@ textarea{min-height:88px;resize:vertical}
<!-- 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>
<h2 data-i18n="req_title">Accedi al portale</h2>
<p class="sub" data-i18n="req_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>
<label for="email" data-i18n="label_email">Email</label>
<input type="email" id="email" inputmode="email" autocomplete="email" placeholder="nome@azienda.it" data-i18n-ph="ph_email" 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>
<button class="btn" id="btn-request" data-i18n="btn_request">Invia link di accesso</button>
<p class="muted"><span data-i18n="req_privacy_pre">Trattiamo la tua email solo per gestire il questionario.</span> <a href="#" id="privacy-link" data-i18n="req_privacy_link">Informativa privacy</a><span data-i18n="req_privacy_post">.</span></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>
<h2 data-i18n="verify_title">Controlla la tua email</h2>
<p class="sub"><span data-i18n="verify_sub_pre">Abbiamo inviato un link di accesso e un codice a</span> <strong id="verify-email"></strong><span data-i18n="verify_sub_post">. Apri l'email e tocca il pulsante, oppure inserisci qui sotto il codice.</span></p>
<div class="msg info"><span data-i18n="verify_spam_pre">Non la trovi? Controlla la cartella</span> <strong data-i18n="verify_spam_strong">spam/posta indesiderata</strong><span data-i18n="verify_spam_post">.</span></div>
<div id="verify-msg"></div>
<label for="otp">Codice di accesso</label>
<label for="otp" data-i18n="label_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>
<button class="btn" id="btn-verify" data-i18n="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>
<button class="btn-ghost" id="btn-other-email" style="float:right" data-i18n="btn_other_email">Usa un'altra email</button>
</div>
<!-- DASHBOARD -->
@@ -112,45 +121,270 @@ textarea{min-height:88px;resize:vertical}
<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>
<button class="btn-ghost" id="btn-logout" data-i18n="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>
<button class="btn-ghost" id="btn-back-dash" data-i18n="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>
<h2 id="fill-title" data-i18n="fill_title_default">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>
<button class="btn" id="btn-submit" data-i18n="btn_submit">Invia definitivo</button>
<div style="height:8px"></div>
<button class="btn btn-secondary" id="btn-draft">Salva bozza</button>
<button class="btn btn-secondary" id="btn-draft" data-i18n="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>
<h2 style="text-align:center" data-i18n="done_title">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>
<button class="btn-ghost" id="btn-done-back" style="display:block;margin:0 auto" data-i18n="btn_done_back">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.
<span data-i18n="foot_l1_pre">Servizio fornito da</span> <strong>NIS2 Agile</strong> <span data-i18n="foot_l1_post">per conto del committente.</span><br>
<span data-i18n="foot_l2">Il committente è titolare del trattamento; NIS2 Agile agisce come responsabile (Art. 28 GDPR).</span><br>
<span data-i18n="foot_l3">Se non eri tu a richiedere questo accesso, ignora l'email ricevuta.</span>
</div>
</div>
<script>
'use strict';
// ── i18n self-contained (IT/EN) — solo "chrome" UI; le domande del questionario sono DATI e NON vengono tradotte ──
const SPI18N = {
it: {
// brand
brand_title:'Portale Fornitori',
brand_sub:'Compilazione questionari di sicurezza NIS2',
// richiesta accesso
req_title:'Accedi al portale',
req_sub:"Inserisci l'email a cui hai ricevuto l'invito. Ti invieremo un link di accesso e un codice.",
label_email:'Email',
ph_email:'nome@azienda.it',
btn_request:'Invia link di accesso',
req_privacy_pre:'Trattiamo la tua email solo per gestire il questionario.',
req_privacy_link:'Informativa privacy',
req_privacy_post:'.',
privacy_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.',
// verifica OTP
verify_title:'Controlla la tua email',
verify_sub_pre:'Abbiamo inviato un link di accesso e un codice a',
verify_sub_post:". Apri l'email e tocca il pulsante, oppure inserisci qui sotto il codice.",
verify_spam_pre:'Non la trovi? Controlla la cartella',
verify_spam_strong:'spam/posta indesiderata',
verify_spam_post:'.',
label_otp:'Codice di accesso',
btn_verify:'Entra nel portale',
btn_resend_email:"Invia di nuovo l'email",
resend_in:'Invia di nuovo tra {s}s',
btn_other_email:"Usa un'altra email",
new_email_sent:'Nuova email inviata.',
// errori richiesta/verifica
err_email_invalid:'Inserisci un indirizzo email valido.',
err_magic_invalid:'Link non valido o scaduto. Richiedi un nuovo accesso.',
err_otp_empty:'Inserisci il codice ricevuto via email.',
err_otp_invalid:'Codice non valido.',
// dashboard
btn_logout:'Esci',
dash_supplier_fallback:'Fornitore',
dash_committente:'Questionari richiesti da {name}',
dash_no_campaigns:'Nessun questionario in attesa. Se hai ricevuto una richiesta, contatta il tuo referente.',
camp_title:'Questionario sicurezza NIS2',
camp_due:'scade il {date}',
cta_receipt:'Vedi ricevuta',
cta_expired:'Scaduto',
cta_resume:'Riprendi',
cta_fill:'Compila',
badge_sent:'Inviato',
badge_expired:'Scaduto',
badge_draft:'In bozza',
badge_todo:'Da compilare',
// compilazione
btn_back_dash:'← Torna ai questionari',
btn_submit:'Invia definitivo',
btn_draft:'Salva bozza',
fill_title_default:'Questionario di sicurezza',
fill_title:'Questionario per {name}',
committente_fallback:'il committente',
due_expired_on:'Scaduto il {date}',
due_on:'Scade il {date}',
multi_hint:'Puoi selezionare più opzioni',
opt_yes:'Sì',
opt_partial:'Parzialmente',
opt_no:'No',
opt_na:'Non applicabile',
scale_min:'minimo',
scale_max:'massimo',
file_formats:'Formati: PDF, JPG, PNG.',
text_ph:'Scrivi qui...',
saving:'Salvataggio…',
draft_saved_at:'Bozza salvata {time}',
save_pending:'Salvataggio in sospeso',
progress_count:'{done} di {total} domande con risposta',
draft_saved:'Bozza salvata.',
err_save:'Errore nel salvataggio.',
err_missing_required:'Mancano {n} risposte obbligatorie.',
confirm_submit:"Confermi l'invio? Dopo l'invio non potrai modificare le risposte.",
err_missing_required_generic:'Mancano risposte obbligatorie.',
err_submit:"Errore nell'invio.",
expired_alert:'Il termine per la compilazione è scaduto.',
// completato
done_title:'Questionario inviato. Grazie!',
done_msg:'Abbiamo ricevuto le tue risposte.',
done_msg_sent:'Abbiamo ricevuto le tue risposte e le abbiamo inviate a {name}.',
done_score:'Punteggio: <strong>{score}/100</strong>',
btn_done_back:'Torna ai questionari',
// footer
foot_l1_pre:'Servizio fornito da',
foot_l1_post:'per conto del committente.',
foot_l2:'Il committente è titolare del trattamento; NIS2 Agile agisce come responsabile (Art. 28 GDPR).',
foot_l3:'Se non eri tu a richiedere questo accesso, ignora l\'email ricevuta.',
// generico
err_network:'Errore di connessione. Riprova.'
},
en: {
// brand
brand_title:'Supplier Portal',
brand_sub:'Complete your NIS2 security questionnaires',
// access request
req_title:'Sign in to the portal',
req_sub:"Enter the email address where you received the invitation. We'll send you an access link and a code.",
label_email:'Email',
ph_email:'name@company.com',
btn_request:'Send access link',
req_privacy_pre:'We process your email only to manage the questionnaire.',
req_privacy_link:'Privacy notice',
req_privacy_post:'.',
privacy_alert:'The client is the data controller of your data (name, email, answers) for the NIS2 security due diligence (Art. 6.1.f/c GDPR). NIS2 Agile acts as data processor (Art. 28). You have the right to access, rectification and erasure. To exercise them, contact the client.',
// OTP verification
verify_title:'Check your email',
verify_sub_pre:'We sent an access link and a code to',
verify_sub_post:'. Open the email and tap the button, or enter the code below.',
verify_spam_pre:"Can't find it? Check your",
verify_spam_strong:'spam/junk folder',
verify_spam_post:'.',
label_otp:'Access code',
btn_verify:'Enter the portal',
btn_resend_email:'Resend the email',
resend_in:'Resend in {s}s',
btn_other_email:'Use a different email',
new_email_sent:'New email sent.',
// request/verify errors
err_email_invalid:'Please enter a valid email address.',
err_magic_invalid:'Invalid or expired link. Please request access again.',
err_otp_empty:'Enter the code you received by email.',
err_otp_invalid:'Invalid code.',
// dashboard
btn_logout:'Log out',
dash_supplier_fallback:'Supplier',
dash_committente:'Questionnaires requested by {name}',
dash_no_campaigns:'No questionnaires pending. If you received a request, please contact your reference person.',
camp_title:'NIS2 security questionnaire',
camp_due:'due {date}',
cta_receipt:'View receipt',
cta_expired:'Expired',
cta_resume:'Resume',
cta_fill:'Fill in',
badge_sent:'Submitted',
badge_expired:'Expired',
badge_draft:'Draft',
badge_todo:'To complete',
// filling
btn_back_dash:'← Back to questionnaires',
btn_submit:'Submit final',
btn_draft:'Save draft',
fill_title_default:'Security questionnaire',
fill_title:'Questionnaire for {name}',
committente_fallback:'the client',
due_expired_on:'Expired on {date}',
due_on:'Due {date}',
multi_hint:'You can select multiple options',
opt_yes:'Yes',
opt_partial:'Partially',
opt_no:'No',
opt_na:'Not applicable',
scale_min:'minimum',
scale_max:'maximum',
file_formats:'Formats: PDF, JPG, PNG.',
text_ph:'Type here...',
saving:'Saving…',
draft_saved_at:'Draft saved {time}',
save_pending:'Save pending',
progress_count:'{done} of {total} questions answered',
draft_saved:'Draft saved.',
err_save:'Error while saving.',
err_missing_required:'{n} required answers are missing.',
confirm_submit:'Confirm submission? After submitting you will not be able to change your answers.',
err_missing_required_generic:'Required answers are missing.',
err_submit:'Error during submission.',
expired_alert:'The deadline for completion has passed.',
// completed
done_title:'Questionnaire submitted. Thank you!',
done_msg:'We have received your answers.',
done_msg_sent:'We have received your answers and sent them to {name}.',
done_score:'Score: <strong>{score}/100</strong>',
btn_done_back:'Back to questionnaires',
// footer
foot_l1_pre:'Service provided by',
foot_l1_post:'on behalf of the client.',
foot_l2:'The client is the data controller; NIS2 Agile acts as data processor (Art. 28 GDPR).',
foot_l3:'If you did not request this access, please ignore the email you received.',
// generic
err_network:'Connection error. Please try again.'
}
};
let SP_LANG = sessionStorage.getItem('sp_lang') || (navigator.language||'it').slice(0,2);
if(SP_LANG!=='en') SP_LANG='it';
// spt(key, params) — ritorna la stringa tradotta con sostituzioni semplici {placeholder}
function spt(k,params){
let s = (SPI18N[SP_LANG]&&SPI18N[SP_LANG][k]) || SPI18N.it[k] || k;
if(params) for(const p in params){ s = s.replace(new RegExp('\\{'+p+'\\}','g'), params[p]); }
return s;
}
// applica le traduzioni al testo statico marcato con data-i18n / data-i18n-ph
function spApplyI18n(){
document.querySelectorAll('[data-i18n]').forEach(e=>{ e.textContent = spt(e.getAttribute('data-i18n')); });
document.querySelectorAll('[data-i18n-ph]').forEach(e=>{ e.setAttribute('placeholder', spt(e.getAttribute('data-i18n-ph'))); });
const it=document.getElementById('lang-it'), en=document.getElementById('lang-en');
if(it) it.classList.toggle('active', SP_LANG==='it');
if(en) en.classList.toggle('active', SP_LANG==='en');
document.documentElement.setAttribute('lang', SP_LANG);
}
// toggle lingua: imposta SP_LANG, persiste, riapplica statico + ri-renderizza il dinamico già presente
function spSetLang(lang){
SP_LANG = (lang==='en') ? 'en' : 'it';
sessionStorage.setItem('sp_lang', SP_LANG);
spApplyI18n();
// ri-render delle parti dinamiche eventualmente visibili (le funzioni di render NON sono modificate)
if(typeof SP_LAST_DASH==='object' && SP_LAST_DASH && !document.getElementById('view-dash').classList.contains('hidden')){ renderDash(SP_LAST_DASH); }
if(CURRENT && !document.getElementById('view-fill').classList.contains('hidden')){
if($('fill-title') && SP_LAST_FILL_COMM!=null){ $('fill-title').textContent = SP_LAST_FILL_COMM ? spt('fill_title',{name:SP_LAST_FILL_COMM}) : spt('fill_title',{name:spt('committente_fallback')}); }
renderDueBar(); renderQuestions();
}
if(!document.getElementById('view-done').classList.contains('hidden') && SP_LAST_DONE){ renderDone(SP_LAST_DONE.d, SP_LAST_DONE.result); }
// se il timer "resend" è attivo lo lasciamo aggiornare da solo; se è già scaduto ne aggiorniamo l'etichetta
const rb=$('btn-resend'); if(rb && !rb.disabled){ rb.textContent = spt('btn_resend_email'); }
}
let SP_LAST_DASH = null; // memorizza l'ultimo data della dashboard per il re-render al cambio lingua
let SP_LAST_FILL_COMM = null; // nome committente per il titolo del fill
let SP_LAST_DONE = null; // {d, result} per il re-render della schermata "completato"
const API = '/api/supplier-portal';
let TOKEN = sessionStorage.getItem('sp_token') || '';
let RESEND_TIMER = null;
@@ -169,13 +403,14 @@ async function api(method,path,body,auth){
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};}
catch(e){return{success:false,message:spt('err_network'),_net:true};}
json._status=res.status;
return json;
}
// ── Boot: magic-link? token? ──
(async function boot(){
spApplyI18n();
const params=new URLSearchParams(location.search);
const magic=params.get('magic');
if(magic){
@@ -184,7 +419,7 @@ async function api(method,path,body,auth){
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');
msg($('req-msg'),(r.message||spt('err_magic_invalid')),'err');
return;
}
if(TOKEN){
@@ -199,7 +434,7 @@ async function api(method,path,body,auth){
// ── 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;}
if(!email||email.indexOf('@')<0){msg($('req-msg'),spt('err_email_invalid'),'err');return;}
this.disabled=true;
const r=await api('POST','/request-otp',{email},false);
this.disabled=false;
@@ -215,7 +450,7 @@ $('email').addEventListener('keydown',e=>{if(e.key==='Enter')$('btn-request').cl
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--;};
const upd=()=>{btn.textContent=s>0?spt('resend_in',{s:s}):spt('btn_resend_email');if(s<=0){btn.disabled=false;clearInterval(RESEND_TIMER);}s--;};
upd();RESEND_TIMER=setInterval(upd,1000);
}
$('btn-resend').onclick=async function(){
@@ -223,7 +458,7 @@ $('btn-resend').onclick=async function(){
if(!email)return;
await api('POST','/request-otp',{email},false);
startResendCountdown();
msg($('verify-msg'),'Nuova email inviata.','ok');
msg($('verify-msg'),spt('new_email_sent'),'ok');
};
$('btn-other-email').onclick=()=>{show('request');$('email').focus();};
@@ -231,13 +466,13 @@ $('btn-other-email').onclick=()=>{show('request');$('email').focus();};
$('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;}
if(!code){msg($('verify-msg'),spt('err_otp_empty'),'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.';
let m=r.message||spt('err_otp_invalid');
msg($('verify-msg'),m,'err');
$('otp').value='';$('otp').focus();
};
@@ -253,46 +488,48 @@ $('btn-logout').onclick=()=>{TOKEN='';sessionStorage.removeItem('sp_token');sess
// ── Dashboard ──
function renderDash(data){
SP_LAST_DASH=data; // per il re-render al cambio lingua
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';
$('dash-supplier').textContent=sup.name||spt('dash_supplier_fallback');
$('dash-committente').textContent=comm.name?spt('dash_committente',{name:comm.name}):'';
$('brand-title').textContent=comm.name||spt('brand_title');
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>';}
if(camps.length===0){html='<div class="msg info">'+esc(spt('dash_no_campaigns'))+'</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 cta=(c.status==='completed')?spt('cta_receipt'):(c.status==='expired'?spt('cta_expired'):(c.status==='in_progress'?spt('cta_resume'):spt('cta_fill')));
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>';
html+='<div class="camp"><div><div style="font-weight:600">'+esc(spt('camp_title'))+'</div><div style="font-size:.8rem;color:var(--gray-500)">'+b+(c.due_at?(' · '+esc(spt('camp_due',{date:fmtDate(c.due_at)}))):'')+'</div></div>'+
'<button class="btn" style="width:auto;min-height:40px;padding:8px 16px" onclick="openCampaign('+c.id+')" '+dis+'>'+esc(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>';
if(c.status==='completed')return '<span class="badge badge-ok">'+esc(spt('badge_sent'))+'</span>';
if(c.status==='expired')return '<span class="badge badge-danger">'+esc(spt('badge_expired'))+'</span>';
if(c.status==='in_progress')return '<span class="badge badge-warn">'+esc(spt('badge_draft'))+'</span>';
return '<span class="badge badge-neutral">'+esc(spt('badge_todo'))+'</span>';
}
function fmtDate(d){try{return new Date(d).toLocaleDateString('it-IT');}catch(e){return d;}}
function fmtDate(d){try{return new Date(d).toLocaleDateString(SP_LANG==='en'?'en-GB':'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.');}
if(r._status===410){alert(spt('expired_alert'));}
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');
SP_LAST_FILL_COMM=(d.committente&&d.committente.name)||''; // per il re-render del titolo al cambio lingua
$('fill-title').textContent=SP_LAST_FILL_COMM?spt('fill_title',{name:SP_LAST_FILL_COMM}):spt('fill_title',{name:spt('committente_fallback')});
renderDueBar();
renderQuestions();
msg($('fill-msg'),'','');
@@ -303,7 +540,7 @@ 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>';}
html='<span class="badge '+cls+'">'+esc(days<0?spt('due_expired_on',{date:fmtDate(due)}):spt('due_on',{date:fmtDate(due)}))+'</span>';}
$('fill-due').innerHTML=html;
}
@@ -327,29 +564,29 @@ function renderQuestions(){
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');
return optGroup(name,[['yes',spt('opt_yes')],['partial',spt('opt_partial')],['no',spt('opt_no')],['not_applicable',spt('opt_na')]],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');
return '<div style="font-size:.75rem;color:var(--gray-400);margin-bottom:6px">'+esc(spt('multi_hint'))+'</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>';
h+='<div class="scale-ends"><span>1 = '+esc(ends.min||ends['1']||spt('scale_min'))+'</span><span>5 = '+esc(ends.max||ends['5']||spt('scale_max'))+'</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>';
return '<div class="muted">'+esc(spt('file_formats'))+' <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>';
return '<textarea name="'+name+'" data-qi="'+i+'" placeholder="'+esc(spt('text_ph'))+'">'+(val!=null?esc(val):'')+'</textarea>';
}
function optGroup(name,opts,val,kind){
@@ -397,13 +634,13 @@ function collect(){
function collectAndAutosave(){
updateProgress();
$('fill-savestate').textContent='Salvataggio…';
$('fill-savestate').textContent=spt('saving');
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';
$('fill-savestate').textContent=r.success?spt('draft_saved_at',{time:new Date().toLocaleTimeString(SP_LANG==='en'?'en-GB':'it-IT').slice(0,5)}):spt('save_pending');
},800);
}
@@ -411,13 +648,13 @@ 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';
$('fill-count').textContent=spt('progress_count',{done:done,total:total});
}
$('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');
msg($('fill-msg'),r.success?spt('draft_saved'):spt('err_save'),r.success?'ok':'err');
};
$('btn-submit').onclick=async function(){
@@ -425,30 +662,31 @@ $('btn-submit').onclick=async function(){
// 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;
if(missing.length){msg($('fill-msg'),spt('err_missing_required',{n:missing.length}),'err');return;}
if(!confirm(spt('confirm_submit')))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');
if(r.data&&r.data.missing){msg($('fill-msg'),spt('err_missing_required_generic'),'err');return;}
msg($('fill-msg'),r.message||spt('err_submit'),'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){
SP_LAST_DONE={d:d,result:result}; // per il re-render al cambio lingua
const comm=(d&&d.committente&&d.committente.name)||'';
$('done-msg').textContent='Abbiamo ricevuto le tue risposte'+(comm?(' e le abbiamo inviate a '+comm):'')+'.';
$('done-msg').textContent=comm?spt('done_msg_sent',{name:comm}):spt('done_msg');
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>';
s='<div class="msg info" style="text-align:center">'+spt('done_score',{score:result.score})+'</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.');};
$('privacy-link').onclick=function(e){e.preventDefault();alert(spt('privacy_alert'));};
</script>
</body>
</html>