diff --git a/apps/ops/src/pages/EventsPage.vue b/apps/ops/src/pages/EventsPage.vue
index 73c8807..ec1c709 100644
--- a/apps/ops/src/pages/EventsPage.vue
+++ b/apps/ops/src/pages/EventsPage.vue
@@ -156,6 +156,9 @@
+
+
+
Programme
@@ -542,7 +545,7 @@ const uploadingAtt = ref(false)
function attLangLabel (l) { return l === 'fr' ? 'FR' : l === 'en' ? 'EN' : 'Les deux' }
function attLangColor (l) { return l === 'fr' ? 'blue-7' : l === 'en' ? 'deep-orange-7' : 'grey-6' }
-const emptyLang = () => ({ name: '', tagline: '', when: '', body: '', program: [], program_line: '', limited: '', closing: '', notes: [] })
+const emptyLang = () => ({ name: '', tagline: '', when: '', address: '', body: '', program: [], program_line: '', limited: '', closing: '', notes: [] })
const emptyForm = () => ({ id: '', active: true, date_iso: '', badge_top: '', badge_bottom: '', capacity: 0, email_template: '', fr: emptyLang(), en: emptyLang() })
const form = ref(emptyForm())
@@ -581,7 +584,7 @@ async function openEdit () {
for (const lg of ['fr', 'en']) {
const src = c[lg] || {}
f[lg] = {
- name: src.name || '', tagline: src.tagline || '', when: src.when || '', body: src.body || '',
+ name: src.name || '', tagline: src.tagline || '', when: src.when || '', address: src.address || '', body: src.body || '',
program: Array.isArray(src.program) ? src.program.map(p => ({ icon: p.icon || '', label: p.label || '', sub: p.sub || '' })) : [],
program_line: src.program_line || '', limited: src.limited || '', closing: src.closing || '',
notes: Array.isArray(src.notes) ? [...src.notes] : [],
@@ -605,7 +608,7 @@ async function openInvitePreview () { invitePreviewOpen.value = true; await load
function cleanLang (c) {
return {
- name: (c.name || '').trim(), tagline: (c.tagline || '').trim(), when: (c.when || '').trim(), body: (c.body || '').trim(),
+ name: (c.name || '').trim(), tagline: (c.tagline || '').trim(), when: (c.when || '').trim(), address: (c.address || '').trim(), body: (c.body || '').trim(),
program: (c.program || []).filter(p => (p.label || '').trim() || (p.icon || '').trim()),
program_line: (c.program_line || '').trim(), limited: (c.limited || '').trim(), closing: (c.closing || '').trim(),
notes: (c.notes || []).map(n => (n || '').trim()).filter(Boolean),
diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js
index 3cd7c70..834a91b 100644
--- a/services/targo-hub/lib/events.js
+++ b/services/targo-hub/lib/events.js
@@ -119,6 +119,7 @@ const SEED_CONTENT = {
name: 'Targo fête ses 20 ans',
tagline: "Toute l'équipe de Targo est heureuse de vous inviter à notre grande fête d'anniversaire !",
when: '1ᵉʳ août, de 10 h à 15 h',
+ address: 'À nos bureaux : 1867 chemin de la rivière, Ste-Clotilde',
body: "Joignez-vous à nous pour une journée de plaisir, de rencontres et de célébration en compagnie des employés de Targo et de leurs familles.",
program: [
{ icon: '🚚', label: 'Food truck', sub: 'repas inclus' },
@@ -139,6 +140,7 @@ const SEED_CONTENT = {
name: 'Targo turns 20!',
tagline: 'The whole Targo team is happy to invite you to our big anniversary celebration!',
when: 'August 1st, 10 a.m. to 3 p.m.',
+ address: 'At our offices: 1867 chemin de la rivière, Ste-Clotilde',
body: 'Join us for a day of fun, meeting people and celebrating alongside the Targo team and their families.',
program: [
{ icon: '🚚', label: 'Food truck', sub: 'meal included' },
@@ -224,6 +226,7 @@ function sanitizeLangContent (c) {
name: String(c.name || '').slice(0, 160),
tagline: String(c.tagline || '').slice(0, 300),
when: String(c.when || '').slice(0, 120),
+ address: String(c.address || '').slice(0, 200),
body: String(c.body || '').slice(0, 1200),
program: sanitizeProgram(c.program),
program_line: String(c.program_line || '').slice(0, 200),
@@ -299,9 +302,14 @@ function keyFor (resolved, form) {
}
// ── Lien perso + courriel d'invitation ─────────────────────────────────────
-function rsvpLink (eventId, customerId, name, email, ttlHours = 60 * 24) {
+// `lang` (optionnel) → ajouté en ?lang= EXPLICITE sur l'URL : la page RSVP priorise TOUJOURS ce
+// paramètre de requête sur la langue du jeton (cf handle() : `if (!url.searchParams.get('lang') && p.lang)`).
+// Sans ça, la version EN du courriel pointait vers la même URL que la FR (défaut FR). Nécessaire
+// même si le modèle éditable (Unlayer) ne permet pas de fixer un href différent par langue.
+function rsvpLink (eventId, customerId, name, email, ttlHours = 60 * 24, lang = '') {
const tok = generateCustomerToken(customerId, name, email, ttlHours)
- return `${pub()}/rsvp/${encodeURIComponent(eventId)}?t=${encodeURIComponent(tok)}`
+ const l = lang ? `&lang=${encodeURIComponent(normLang(lang))}` : ''
+ return `${pub()}/rsvp/${encodeURIComponent(eventId)}?t=${encodeURIComponent(tok)}${l}`
}
function inviteSubject (eventId, lang) {
const e = getEvent(eventId); if (!e) return ''
@@ -312,7 +320,7 @@ function inviteTemplateVars (e, lang, name, rsvpUrl) {
const t = e[lang]
return {
firstname: firstName(name), name: name || '', rsvp_url: rsvpUrl, gift_url: rsvpUrl, // gift_url = alias (templates cadeau réutilisables)
- event_name: t.name, tagline: t.tagline, when: t.when, body: t.body, closing: t.closing,
+ event_name: t.name, tagline: t.tagline, when: t.when, address: t.address, body: t.body, closing: t.closing,
year: String(new Date().getFullYear()), lang,
}
}
@@ -345,12 +353,13 @@ function inviteEmailFestive (e, lang, name, rsvpUrl) {
+ `| `
+ ` 🎈🎉🎈 `
+ (badge ? `🎉 ${esc(badge)} 🎉 ` : '')
- + ` |
`
+ + `
`
+ `${esc(t.name)} 🎉`
+ `${esc(t.tagline)} |
`
+ `${esc(t.greet(firstName(name)) || '')} `
+ `${esc(t.body)} |
`
+ (t.when ? ` |
` : '')
+ + (t.address ? `📍 ${esc(t.address)} |
` : '')
+ ((t.program || []).length ? ` |
` : '')
+ (t.program_line ? `${esc(t.program_line)} |
` : '')
+ (t.limited ? ` |
` : '')
@@ -440,7 +449,7 @@ async function sendTest ({ eventId, emails, channel = 'mailjet', from = '', lang
const atts = attachmentPayload(channel, loadSendAttachments(event, lang)) // pièces jointes de la langue testée (+ 'both')
const results = []
for (const em of emails) {
- const link = rsvpLink(eventId, 'TEST-' + em, name || 'Test', em, 24)
+ const link = rsvpLink(eventId, 'TEST-' + em, name || 'Test', em, 24, lang)
const html = inviteEmail(eventId, lang, name, link)
const subject = '[TEST] ' + inviteSubject(eventId, lang)
try { const r = await sendInviteMessage({ channel, to: em, subject, html, from, attachments: atts }); results.push({ email: em, ok: r.ok, error: r.error }) }
@@ -490,7 +499,7 @@ async function sendEventCampaignAsync (id, eventId, channel, from) {
if (r.status !== 'pending') continue
const lang = normLang(r.language)
const name = ((r.firstname || '') + ' ' + (r.lastname || '')).trim()
- const rsvpUrl = rsvpLink(eventId, r.customer_id || ('x-' + r.email), name, r.email)
+ const rsvpUrl = rsvpLink(eventId, r.customer_id || ('x-' + r.email), name, r.email, 60 * 24, lang)
r.rsvp_url = rsvpUrl
const html = inviteEmail(eventId, lang, name, rsvpUrl)
const subject = inviteSubject(eventId, lang)
@@ -749,7 +758,7 @@ 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}
.badge b{font-size:1.35rem}.badge span{font-size:.6rem;letter-spacing:1px}
.badge.emoji{font-size:2rem}
-.logo{height:34px;width:auto;display:block;margin:0 auto 14px}
+.logo{height:34px;width:auto;display:block;margin:0 auto 34px}
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}
.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}
@@ -757,6 +766,7 @@ 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(--ink);color:#fff;font-weight:700;
font-size:1.12rem;border-radius:14px;padding:13px 18px;margin:4px 0 16px}
.when .cal{font-size:1.3rem}
+.addr{display:flex;align-items:center;justify-content:center;gap:6px;color:#374151;font-weight:600;font-size:.92rem;margin:-8px 0 16px;text-align:center}
.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-ic{font-size:1.7rem;line-height:1}
@@ -799,6 +809,7 @@ button:hover{filter:brightness(1.05)}button:active{transform:translateY(1px)}but
${t.when ? `
📅${esc(t.when)}
` : ''}
+ ${t.address ? `
📍 ${esc(t.address)}
` : ''}
${esc(t.body)}
${program ? `
${program}
` : ''}
${t.program_line ? `
${esc(t.program_line)}
` : ''}