[FEAT] licenseExt: sezione dati destinatario pre-compila form + link pronto + modale con recipient data

This commit is contained in:
DevEnv nis2-agile 2026-03-10 12:00:26 +01:00
parent 7bb92b1971
commit e02e0e21d0
2 changed files with 72 additions and 12 deletions

View File

@ -210,6 +210,12 @@ class InviteController extends BaseController
$row['used_by_org'] = $org;
}
// Parsa metadata per esporre recipient strutturato
if (!empty($row['metadata'])) {
$meta = json_decode($row['metadata'], true) ?? [];
$row['metadata_recipient'] = $meta['recipient'] ?? null;
}
$this->jsonSuccess($row);
}

View File

@ -366,8 +366,9 @@ body { background: var(--bg-main); }
<input type="text" id="gLabel" placeholder="Es: NIS2 Starter — Ordine 2026-042">
</div>
<div class="form-group">
<label>Destinatario / Reseller</label>
<label>Destinatario interno (riferimento)</label>
<input type="text" id="gIssuedTo" placeholder="nome@azienda.it o ragione sociale">
<div class="form-hint">Solo riferimento interno — non visibile al cliente</div>
</div>
<div class="form-group">
<label>Nome reseller (riferimento interno)</label>
@ -387,6 +388,32 @@ body { background: var(--bg-main); }
<label>Note interne marketing</label>
<textarea id="gNotes" rows="2" placeholder="Ordine di riferimento, condizioni speciali, sconto applicato..."></textarea>
</div>
</div>
<!-- Dati destinatario — pre-compilano il form di registrazione -->
<h2 style="margin-top:1.5rem">Dati destinatario <span style="font-size:.78rem;font-weight:400;color:var(--text-secondary)">— pre-compilano il form di registrazione del cliente</span></h2>
<div class="gen-grid">
<div class="form-group">
<label>Nome *</label>
<input type="text" id="gRcpFirst" placeholder="Es: Cristiano">
</div>
<div class="form-group">
<label>Cognome *</label>
<input type="text" id="gRcpLast" placeholder="Es: Benassati">
</div>
<div class="form-group">
<label>Email destinatario *</label>
<input type="email" id="gRcpEmail" placeholder="presidenza@agile.software">
<div class="form-hint">Il cliente vedrà questa email pre-compilata nel form</div>
</div>
<div class="form-group">
<label>P.IVA azienda</label>
<input type="text" id="gRcpVat" placeholder="07776161213" maxlength="11">
<div class="form-hint">Pre-compila il campo P.IVA nel form di registrazione</div>
</div>
</div>
<div class="gen-grid" style="margin-top:1rem">
<div class="form-group">
<label>Limita a P.IVA (opzionale)</label>
<input type="text" id="gRestVat" placeholder="02345678901" maxlength="11">
@ -697,6 +724,16 @@ async function generateLicense() {
const email = document.getElementById('gRestEmail').value.trim();
if (email) body.restrict_email = email;
// Dati destinatario — pre-compilano il form di registrazione
const rcpFirst = document.getElementById('gRcpFirst').value.trim();
const rcpLast = document.getElementById('gRcpLast').value.trim();
const rcpEmail = document.getElementById('gRcpEmail').value.trim();
const rcpVat = document.getElementById('gRcpVat').value.trim();
if (rcpFirst) body.recipient_first_name = rcpFirst;
if (rcpLast) body.recipient_last_name = rcpLast;
if (rcpEmail) body.recipient_email = rcpEmail;
if (rcpVat) body.recipient_vat = rcpVat;
const btn = document.getElementById('genBtn');
const ldr = document.getElementById('genLoading');
btn.style.display = 'none'; ldr.style.display = '';
@ -718,20 +755,27 @@ async function generateLicense() {
function renderGenResult(invites) {
const res = document.getElementById('genResult');
const list = document.getElementById('genTokenList');
list.innerHTML = invites.map((inv, i) => `
<div style="margin-bottom:.75rem">
<div style="font-size:.75rem;color:var(--text-secondary);margin-bottom:.2rem">
list.innerHTML = invites.map((inv, i) => {
const r = inv.recipient;
const recipientLine = r ? `<div style="font-size:.72rem;color:#22c55e;margin-top:.2rem">
👤 ${[r.first_name, r.last_name].filter(Boolean).join(' ')}${r.email ? ` &lt;${r.email}&gt;` : ''}${r.vat ? ` · P.IVA ${r.vat}` : ''} — form pre-compilato ✓
</div>` : '';
return `
<div style="margin-bottom:.75rem;padding:.75rem;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.2);border-radius:8px">
<div style="font-size:.75rem;color:var(--text-secondary);margin-bottom:.3rem">
Licenza #${i+1} — ${inv.plan} · ${inv.duration_months} mesi · ID: ${inv.id}
</div>
<div class="token-val" id="tok-${i}">${inv.token}</div>
<div style="font-size:.72rem;color:var(--text-secondary)">
URL: <a href="${inv.invite_url}" style="color:var(--primary)">${inv.invite_url}</a>
· Scade invito: ${new Date(inv.expires_at).toLocaleDateString('it-IT')}
${inv.max_users_per_org ? ` · Max utenti/org: ${inv.max_users_per_org}` : ''}
${inv.price_eur ? ` · € ${inv.price_eur}` : ''}
</div>
</div>
`).join('');
<div class="token-val" id="tok-${i}">${inv.token}</div>
<div style="font-size:.72rem;color:var(--text-secondary);margin-top:.3rem">
🔗 Link da inviare al cliente: <a href="${inv.invite_url}" target="_blank" style="color:var(--primary)">${inv.invite_url}</a>
<button class="act-btn" style="font-size:.7rem;padding:.2rem .5rem;margin-left:.5rem" onclick="navigator.clipboard.writeText('${inv.invite_url}').then(()=>showToast('Link copiato','ok'))">Copia link</button>
</div>
${recipientLine}
</div>`;
}).join('');
res.classList.add('show');
res.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
@ -757,7 +801,8 @@ function exportCsv() {
}
function resetForm() {
['gLabel','gIssuedTo','gReseller','gNotes','gRestVat','gRestEmail','gPrice','gMaxUsers'].forEach(id => {
['gLabel','gIssuedTo','gReseller','gNotes','gRestVat','gRestEmail','gPrice','gMaxUsers',
'gRcpFirst','gRcpLast','gRcpEmail','gRcpVat'].forEach(id => {
document.getElementById(id).value = '';
});
document.getElementById('gPlan').value = 'professional';
@ -819,7 +864,7 @@ async function showDetail(id) {
<div class="detail-item"><div class="dl">Reseller</div><div class="dv">${inv.reseller_name || '—'}</div></div>
</div>
<div class="detail-row" style="margin-bottom:1rem">
<div class="detail-item"><div class="dl">Destinatario</div><div class="dv">${inv.issued_to || '—'}</div></div>
<div class="detail-item"><div class="dl">Rif. interno</div><div class="dv">${inv.issued_to || '—'}</div></div>
<div class="detail-item"><div class="dl">Creato</div><div class="dv">${new Date(inv.created_at).toLocaleString('it-IT')}</div></div>
${inv.used_at ? `<div class="detail-item"><div class="dl">Usato il</div><div class="dv">${new Date(inv.used_at).toLocaleString('it-IT')}</div></div>` : ''}
</div>
@ -834,6 +879,15 @@ async function showDetail(id) {
<strong>${inv.used_by_org.name}</strong>
<span style="font-size:.78rem;color:var(--text-secondary);margin-left:.5rem">${inv.used_by_org.sector || ''} · ${inv.used_by_org.nis2_entity_type || ''}</span>
</div>` : ''}
${inv.metadata_recipient ? (() => { const r = inv.metadata_recipient; return `
<div style="background:rgba(34,197,94,.07);border:1px solid rgba(34,197,94,.25);border-radius:8px;padding:.75rem 1rem;margin-bottom:1rem">
<div style="font-size:.75rem;color:var(--text-secondary);margin-bottom:.4rem">👤 Dati destinatario (pre-compilano il form)</div>
<div style="font-size:.88rem">
${[r.first_name, r.last_name].filter(Boolean).join(' ')}
${r.email ? `<span style="color:var(--primary);margin-left:.5rem">${r.email}</span>` : ''}
${r.vat ? `<span style="color:var(--text-secondary);margin-left:.5rem">P.IVA: ${r.vat}</span>` : ''}
</div>
</div>`; })() : ''}
${inv.notes ? `<div style="font-size:.82rem;color:var(--text-secondary);border-top:1px solid var(--border);padding-top:.75rem"><strong>Note:</strong> ${inv.notes}</div>` : ''}
<div style="display:flex;gap:.5rem;margin-top:1rem;flex-wrap:wrap">
${inv.status === 'pending' ? `<button class="act-btn danger" onclick="revokeInvite(${inv.id})">Revoca licenza</button>` : ''}