feat(events): palette TARGO uniquement + téléphone visible + IP visiteur + CSV complet
- Couleurs restreintes à la charte TARGO (noir/vert/blanc/gris + bleu en haut) sur le courriel ET la page RSVP : bandeau supérieur bleu #1a86c7, accents/CTA verts, texte noir/gris, fond blanc, programme en pastilles vert/bleu/gris. Retrait orange/jaune/rose/rouge. - Admin : le téléphone saisi s'affiche sous le courriel (n'apparaissait pas avant). - Hub : capture de l'adresse IP du visiteur à l'inscription (x-forwarded-for → socket). - CSV enrichi = TOUTES les infos : client, n° client, ID compte, nom au dossier, téléphone, personnes, courriel, allergies, confiance, signaux, langue, IP, créé le, répondu le. Déployé hub + SPA 2026-07-06. Vérifié prod : palette conforme, téléphone + IP stockés (819 555-9999 / 96.125.192.22), test envoyé OK. 21/21 tests unitaires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c8cb6bfa5d
commit
632b466310
|
|
@ -75,6 +75,12 @@
|
||||||
<template #body-cell-party="props">
|
<template #body-cell-party="props">
|
||||||
<q-td :props="props" class="text-center"><q-chip dense square color="green-1" text-color="green-9" class="text-weight-bold">{{ props.value }}</q-chip></q-td>
|
<q-td :props="props" class="text-center"><q-chip dense square color="green-1" text-color="green-9" class="text-weight-bold">{{ props.value }}</q-chip></q-td>
|
||||||
</template>
|
</template>
|
||||||
|
<template #body-cell-email="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<div>{{ props.value || '—' }}</div>
|
||||||
|
<div v-if="props.row.phone" class="text-caption text-grey-7"><q-icon name="call" size="12px" class="q-mr-xs" />{{ props.row.phone }}</div>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
<template #body-cell-allergies="props">
|
<template #body-cell-allergies="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
<span v-if="props.value" class="text-orange-9">⚠ {{ props.value }}</span>
|
<span v-if="props.value" class="text-orange-9">⚠ {{ props.value }}</span>
|
||||||
|
|
@ -205,9 +211,9 @@ function copy (txt) {
|
||||||
|
|
||||||
function exportRows () {
|
function exportRows () {
|
||||||
const conf = { confirmed: 'Confirmé', probable: 'Probable' }
|
const conf = { confirmed: 'Confirmé', probable: 'Probable' }
|
||||||
const headers = ['Client', 'N° client', 'Téléphone', 'Personnes', 'Courriel', 'Allergies', 'Compte', 'Langue', 'Répondu le']
|
const headers = ['Client', 'N° client', 'ID compte', 'Nom au dossier', 'Téléphone', 'Personnes', 'Courriel', 'Allergies', 'Confiance', 'Signaux', 'Langue', 'IP', 'Créé le', 'Répondu le']
|
||||||
const data = filtered.value.map(r => [r.customer_name || '', r.customer_number || '', r.phone || '', r.party_size || 0, r.email || '', r.allergies || '', conf[r.confidence] || 'À vérifier', r.lang || '', fmtDate(r.updated)])
|
const data = filtered.value.map(r => [r.customer_name || '', r.customer_number || '', r.customer_id || '', r.account_name || '', r.phone || '', r.party_size || 0, r.email || '', r.allergies || '', conf[r.confidence] || 'À vérifier', (r.signals || []).join(' + '), r.lang || '', r.ip || '', fmtDate(r.created), fmtDate(r.updated)])
|
||||||
const totalRow = ['TOTAL', '', '', filtered.value.reduce((s, r) => s + (r.party_size || 0), 0), '', '', '', '', '']
|
const totalRow = ['TOTAL', '', '', '', '', filtered.value.reduce((s, r) => s + (r.party_size || 0), 0), '', '', '', '', '', '', '', '']
|
||||||
exportCsv(`inscriptions_${EVENT_ID}`, headers, data, { totalRow })
|
exportCsv(`inscriptions_${EVENT_ID}`, headers, data, { totalRow })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,8 +183,8 @@ function inviteEmail (eventId, lang, name, rsvpUrl) {
|
||||||
const e = getEvent(eventId); if (!e) return ''
|
const e = getEvent(eventId); if (!e) return ''
|
||||||
lang = normLang(lang); const t = e[lang]
|
lang = normLang(lang); const t = e[lang]
|
||||||
const merci = lang === 'en' ? 'Thank you for your trust over the years! 💚' : 'Merci de votre confiance au fil des années ! 💚'
|
const merci = lang === 'en' ? 'Thank you for your trust over the years! 💚' : 'Merci de votre confiance au fil des années ! 💚'
|
||||||
const CIRC = ['#e6f7ea', '#e3f2fd', '#ede7f6', '#fce4ec', '#fff3e0']
|
const CIRC = ['#e8f5ec', '#e6f2f9', '#f1f5f9', '#e8f5ec', '#e6f2f9'] // vert / bleu / gris (palette TARGO)
|
||||||
const LBL = ['#1e8e3e', '#1976d2', '#7e57c2', '#d81b60', '#ef6c00']
|
const LBL = ['#1e8e3e', '#1a6f9e', '#4b5563', '#1e8e3e', '#1a6f9e']
|
||||||
const prog = t.program.map((p, i) =>
|
const prog = t.program.map((p, i) =>
|
||||||
`<td align="center" valign="top" style="padding:6px 3px">`
|
`<td align="center" valign="top" style="padding:6px 3px">`
|
||||||
+ `<div style="width:50px;height:50px;line-height:50px;border-radius:25px;background:${CIRC[i % 5]};font-size:25px;margin:0 auto">${p.icon}</div>`
|
+ `<div style="width:50px;height:50px;line-height:50px;border-radius:25px;background:${CIRC[i % 5]};font-size:25px;margin:0 auto">${p.icon}</div>`
|
||||||
|
|
@ -193,19 +193,19 @@ function inviteEmail (eventId, lang, name, rsvpUrl) {
|
||||||
+ `<body style="margin:0;background:#eef4f8;font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#1e293b">`
|
+ `<body style="margin:0;background:#eef4f8;font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#1e293b">`
|
||||||
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#eef4f8"><tr><td align="center" style="padding:30px 14px">`
|
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#eef4f8"><tr><td align="center" style="padding:30px 14px">`
|
||||||
+ `<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;background:#fff;border-radius:18px;overflow:hidden;border:1px solid #e2e8f0">`
|
+ `<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;background:#fff;border-radius:18px;overflow:hidden;border:1px solid #e2e8f0">`
|
||||||
+ `<tr><td style="height:10px;background:linear-gradient(90deg,#21a34a 0 20%,#29a3dd 20% 40%,#ffcd05 40% 60%,#f7941d 60% 80%,#173a5e 80% 100%);font-size:0;line-height:0"> </td></tr>`
|
+ `<tr><td style="height:10px;background:#1a86c7;font-size:0;line-height:0"> </td></tr>`
|
||||||
+ `<tr><td align="center" style="padding:22px 30px 0">`
|
+ `<tr><td align="center" style="padding:22px 30px 0">`
|
||||||
+ `<div style="font-size:24px;letter-spacing:6px;line-height:1">🎈🎉🎈</div>`
|
+ `<div style="font-size:24px;letter-spacing:6px;line-height:1">🎈🎉🎈</div>`
|
||||||
+ `<div style="display:inline-block;background:#21a34a;color:#fff;font-weight:800;font-size:12px;letter-spacing:1.5px;padding:6px 16px;border-radius:999px;margin:10px 0 8px">🎉 20 ANS 🎉</div><br>`
|
+ `<div style="display:inline-block;background:#21a34a;color:#fff;font-weight:800;font-size:12px;letter-spacing:1.5px;padding:6px 16px;border-radius:999px;margin:10px 0 8px">🎉 20 ANS 🎉</div><br>`
|
||||||
+ `<img src="${LOGO}" alt="TARGO" width="150" style="width:150px;max-width:150px;height:auto;display:inline-block;border:0;margin:2px 0"></td></tr>`
|
+ `<img src="${LOGO}" alt="TARGO" width="150" style="width:150px;max-width:150px;height:auto;display:inline-block;border:0;margin:2px 0"></td></tr>`
|
||||||
+ `<tr><td align="center" style="padding:8px 26px 0"><h1 style="margin:6px 0 4px;font-size:27px;font-weight:800;color:#173a5e;line-height:1.15">${esc(t.name)} 🎉</h1>`
|
+ `<tr><td align="center" style="padding:8px 26px 0"><h1 style="margin:6px 0 4px;font-size:27px;font-weight:800;color:#1f2937;line-height:1.15">${esc(t.name)} 🎉</h1>`
|
||||||
+ `<p style="margin:0;font-size:15px;font-weight:700;color:#21a34a;line-height:1.4">${esc(t.tagline)}</p></td></tr>`
|
+ `<p style="margin:0;font-size:15px;font-weight:700;color:#21a34a;line-height:1.4">${esc(t.tagline)}</p></td></tr>`
|
||||||
+ `<tr><td style="padding:16px 34px 4px"><p style="margin:0 0 10px;font-size:15px;line-height:1.6;color:#475569">${esc(t.greet(firstName(name)) || '')}</p>`
|
+ `<tr><td style="padding:16px 34px 4px"><p style="margin:0 0 10px;font-size:15px;line-height:1.6;color:#475569">${esc(t.greet(firstName(name)) || '')}</p>`
|
||||||
+ `<p style="margin:0;font-size:15px;line-height:1.6;color:#475569">${esc(t.body)}</p></td></tr>`
|
+ `<p style="margin:0;font-size:15px;line-height:1.6;color:#475569">${esc(t.body)}</p></td></tr>`
|
||||||
+ `<tr><td align="center" style="padding:16px 30px 6px"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td style="background:#173a5e;border-radius:12px;padding:13px 24px;color:#fff;font-size:18px;font-weight:800">📅 ${esc(t.when)}</td></tr></table></td></tr>`
|
+ `<tr><td align="center" style="padding:16px 30px 6px"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td style="background:#1f2937;border-radius:12px;padding:13px 24px;color:#fff;font-size:18px;font-weight:800">📅 ${esc(t.when)}</td></tr></table></td></tr>`
|
||||||
+ `<tr><td style="padding:14px 18px 4px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>${prog}</tr></table></td></tr>`
|
+ `<tr><td style="padding:14px 18px 4px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>${prog}</tr></table></td></tr>`
|
||||||
+ `<tr><td align="center" style="padding:2px 30px 8px"><p style="margin:0;font-size:13px;font-style:italic;color:#94a3b8">${esc(t.program_line)}</p></td></tr>`
|
+ `<tr><td align="center" style="padding:2px 30px 8px"><p style="margin:0;font-size:13px;font-style:italic;color:#94a3b8">${esc(t.program_line)}</p></td></tr>`
|
||||||
+ `<tr><td style="padding:6px 30px 4px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#fff5f5;border:1px solid #fecaca;border-radius:12px"><tr><td style="padding:12px 16px;font-size:14px;line-height:1.6;color:#9a3412">⏳ ${esc(t.limited)}</td></tr></table></td></tr>`
|
+ `<tr><td style="padding:6px 30px 4px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;border:1px solid #e5e7eb;border-radius:12px"><tr><td style="padding:12px 16px;font-size:14px;line-height:1.6;color:#374151">⏳ ${esc(t.limited)}</td></tr></table></td></tr>`
|
||||||
+ `<tr><td align="center" style="padding:20px 30px 6px"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td style="background:#21a34a;border-radius:14px;box-shadow:0 6px 16px rgba(33,163,74,.3)"><a href="${esc(rsvpUrl)}" target="_blank" style="display:inline-block;padding:16px 40px;color:#fff;font-size:17px;font-weight:800;text-decoration:none">${esc(t.submit)} →</a></td></tr></table></td></tr>`
|
+ `<tr><td align="center" style="padding:20px 30px 6px"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td style="background:#21a34a;border-radius:14px;box-shadow:0 6px 16px rgba(33,163,74,.3)"><a href="${esc(rsvpUrl)}" target="_blank" style="display:inline-block;padding:16px 40px;color:#fff;font-size:17px;font-weight:800;text-decoration:none">${esc(t.submit)} →</a></td></tr></table></td></tr>`
|
||||||
+ `<tr><td align="center" style="padding:8px 30px 20px"><p style="margin:0;font-size:15px;font-weight:700;color:#21a34a">${esc(merci)}</p>`
|
+ `<tr><td align="center" style="padding:8px 30px 20px"><p style="margin:0;font-size:15px;font-weight:700;color:#21a34a">${esc(merci)}</p>`
|
||||||
+ `<p style="margin:6px 0 0;font-size:13px;color:#94a3b8">${esc(t.closing)}</p></td></tr>`
|
+ `<p style="margin:6px 0 0;font-size:13px;color:#94a3b8">${esc(t.closing)}</p></td></tr>`
|
||||||
|
|
@ -249,11 +249,11 @@ function page (event, lang, prefill = {}) {
|
||||||
return `<!doctype html><html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
return `<!doctype html><html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>${esc(t.name)}</title>
|
<title>${esc(t.name)}</title>
|
||||||
<style>
|
<style>
|
||||||
:root{--g:#21a34a;--navy:#173a5e;--blue:#29a3dd;--orange:#f7941d;--yellow:#ffcd05;--ink:#1e293b;--muted:#64748b;--line:#e6ebe8}
|
:root{--g:#21a34a;--blue:#1a86c7;--ink:#1f2937;--muted:#6b7280;--line:#e5e7eb}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
body{margin:0;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:var(--ink);
|
body{margin:0;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:var(--ink);
|
||||||
background:radial-gradient(1200px 400px at 50% -120px,#eafbf0 0%,transparent 70%),linear-gradient(180deg,#f7fbff 0%,#ffffff 60%);min-height:100vh}
|
background:radial-gradient(1200px 400px at 50% -120px,#eafbf0 0%,transparent 70%),linear-gradient(180deg,#f7fbff 0%,#ffffff 60%);min-height:100vh}
|
||||||
.confetti{height:8px;background:linear-gradient(90deg,var(--g) 0 20%,var(--blue) 20% 40%,var(--yellow) 40% 60%,var(--orange) 60% 80%,var(--navy) 80% 100%)}
|
.confetti{height:10px;background:var(--blue)}
|
||||||
.wrap{max-width:600px;margin:0 auto;padding:26px 18px 46px}
|
.wrap{max-width:600px;margin:0 auto;padding:26px 18px 46px}
|
||||||
.lang{text-align:right;margin-bottom:6px}
|
.lang{text-align:right;margin-bottom:6px}
|
||||||
.lang a{color:var(--muted);text-decoration:none;font-size:.82rem;border:1px solid var(--line);padding:4px 10px;border-radius:999px}
|
.lang a{color:var(--muted);text-decoration:none;font-size:.82rem;border:1px solid var(--line);padding:4px 10px;border-radius:999px}
|
||||||
|
|
@ -262,23 +262,23 @@ body{margin:0;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-s
|
||||||
background:var(--g);color:#fff;font-weight:800;line-height:1;box-shadow:0 8px 20px rgba(33,163,74,.3);margin-bottom:12px}
|
background:var(--g);color:#fff;font-weight:800;line-height:1;box-shadow:0 8px 20px rgba(33,163,74,.3);margin-bottom:12px}
|
||||||
.badge b{font-size:1.35rem}.badge span{font-size:.6rem;letter-spacing:1px}
|
.badge b{font-size:1.35rem}.badge span{font-size:.6rem;letter-spacing:1px}
|
||||||
.logo{height:34px;width:auto;display:block;margin:0 auto 14px}
|
.logo{height:34px;width:auto;display:block;margin:0 auto 14px}
|
||||||
h1{font-size:1.7rem;font-weight:800;letter-spacing:-.5px;margin:0 0 6px;color:var(--navy)}
|
h1{font-size:1.7rem;font-weight:800;letter-spacing:-.5px;margin:0 0 6px;color:var(--ink)}
|
||||||
.tag{color:var(--g);font-weight:700;font-size:1.02rem;margin:0 0 4px}
|
.tag{color:var(--g);font-weight:700;font-size:1.02rem;margin:0 0 4px}
|
||||||
.card{background:#fff;border:1px solid #eef2f0;border-radius:20px;box-shadow:0 16px 44px rgba(23,58,94,.09);padding:26px 22px;margin-bottom:16px}
|
.card{background:#fff;border:1px solid #eef2f0;border-radius:20px;box-shadow:0 16px 44px rgba(23,58,94,.09);padding:26px 22px;margin-bottom:16px}
|
||||||
p{line-height:1.6;font-size:1rem;margin:0 0 12px;color:#334155}
|
p{line-height:1.6;font-size:1rem;margin:0 0 12px;color:#334155}
|
||||||
.when{display:flex;align-items:center;justify-content:center;gap:10px;background:var(--navy);color:#fff;font-weight:700;
|
.when{display:flex;align-items:center;justify-content:center;gap:10px;background:var(--ink);color:#fff;font-weight:700;
|
||||||
font-size:1.12rem;border-radius:14px;padding:13px 18px;margin:4px 0 16px}
|
font-size:1.12rem;border-radius:14px;padding:13px 18px;margin:4px 0 16px}
|
||||||
.when .cal{font-size:1.3rem}
|
.when .cal{font-size:1.3rem}
|
||||||
.prog-wrap{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin:6px 0 4px}
|
.prog-wrap{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin:6px 0 4px}
|
||||||
.prog{flex:1 1 90px;max-width:110px;text-align:center;background:#f8fafc;border:1px solid #eef2f6;border-radius:14px;padding:12px 6px}
|
.prog{flex:1 1 90px;max-width:110px;text-align:center;background:#f8fafc;border:1px solid #eef2f6;border-radius:14px;padding:12px 6px}
|
||||||
.prog-ic{font-size:1.7rem;line-height:1}
|
.prog-ic{font-size:1.7rem;line-height:1}
|
||||||
.prog-lb{margin-top:6px;font-size:.78rem;font-weight:700;color:var(--navy)}
|
.prog-lb{margin-top:6px;font-size:.78rem;font-weight:700;color:var(--ink)}
|
||||||
.prog-lb span{display:block;font-weight:500;color:var(--muted);font-size:.7rem}
|
.prog-lb span{display:block;font-weight:500;color:var(--muted);font-size:.7rem}
|
||||||
.limited{display:flex;gap:10px;align-items:flex-start;background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:12px 14px;margin:14px 0 2px}
|
.limited{display:flex;gap:10px;align-items:flex-start;background:#f3f4f6;border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin:14px 0 2px}
|
||||||
.limited .i{font-size:1.1rem}
|
.limited .i{font-size:1.1rem}
|
||||||
.limited p{margin:0;font-size:.92rem;color:#9a3412}
|
.limited p{margin:0;font-size:.92rem;color:#374151}
|
||||||
.greet{font-weight:700;color:var(--navy);font-size:1.05rem;margin:0 0 10px}
|
.greet{font-weight:700;color:var(--ink);font-size:1.05rem;margin:0 0 10px}
|
||||||
label{display:block;font-weight:600;font-size:.92rem;color:var(--navy);margin:14px 0 6px}
|
label{display:block;font-weight:600;font-size:.92rem;color:var(--ink);margin:14px 0 6px}
|
||||||
label .opt{font-weight:400;color:var(--muted);font-size:.82rem}
|
label .opt{font-weight:400;color:var(--muted);font-size:.82rem}
|
||||||
input,textarea{width:100%;border:1.5px solid var(--line);border-radius:12px;padding:13px 15px;font:inherit;font-size:1rem;background:#fcfdfc;transition:border-color .15s,box-shadow .15s}
|
input,textarea{width:100%;border:1.5px solid var(--line);border-radius:12px;padding:13px 15px;font:inherit;font-size:1rem;background:#fcfdfc;transition:border-color .15s,box-shadow .15s}
|
||||||
input:focus,textarea:focus{outline:none;border-color:var(--g);box-shadow:0 0 0 4px rgba(33,163,74,.14)}
|
input:focus,textarea:focus{outline:none;border-color:var(--g);box-shadow:0 0 0 4px rgba(33,163,74,.14)}
|
||||||
|
|
@ -428,10 +428,12 @@ async function handle (req, res, method, path, url) {
|
||||||
const r = await matchAndValidate({ token: b.token, number, email, phone, name })
|
const r = await matchAndValidate({ token: b.token, number, email, phone, name })
|
||||||
const key = keyFor(r, { number, email, phone })
|
const key = keyFor(r, { number, email, phone })
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
|
const ip = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim() || (req.socket && req.socket.remoteAddress) || ''
|
||||||
const m = loadResp(eventId)
|
const m = loadResp(eventId)
|
||||||
const prev = m[key] || {}
|
const prev = m[key] || {}
|
||||||
m[key] = {
|
m[key] = {
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
|
ip,
|
||||||
customer_id: r.customer_id,
|
customer_id: r.customer_id,
|
||||||
customer_number: number || (r.customer_id || ''),
|
customer_number: number || (r.customer_id || ''),
|
||||||
customer_name: name || r.customer_name || prev.customer_name || '',
|
customer_name: name || r.customer_name || prev.customer_name || '',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user