diff --git a/apps/ops/src/modules/campaigns/pages/TemplateEditorPage.vue b/apps/ops/src/modules/campaigns/pages/TemplateEditorPage.vue index 672025d..7c09f48 100644 --- a/apps/ops/src/modules/campaigns/pages/TemplateEditorPage.vue +++ b/apps/ops/src/modules/campaigns/pages/TemplateEditorPage.vue @@ -5,17 +5,33 @@
Éditeur de template
+ + Repartir d'un canevas vide (pour composer une nouvelle template depuis zéro) + + + Recharger la dernière version sauvegardée sur disque + - + + Annuler les changements non sauvegardés + + + + + Le mode Visuel est idéal pour composer une nouvelle template depuis un canevas vide + (clique "Vide" ↑). Pour éditer la template existante (tables imbriquées hand-crafted), + passe en mode HTML — GrapesJS ne parse pas toujours les structures complexes. + +
@@ -23,10 +39,16 @@ Édition HTML brute. Les changements sauvegardés ici écrasent ceux de l'éditeur visuel. - Variables disponibles : {{ '{{firstname}}' }}, {{ '{{amount}}' }}, - {{ '{{gift_url}}' }}, {{ '{{description}}' }}, - {{ '{{expiry}}' }}, {{ '{{commitment_months}}' }}. - Blocs conditionnels : {{ '{{#expiry}}...{{/expiry}}' }}. + + + Variables disponibles : {{firstname}}, {{amount}}, + {{gift_url}}, {{description}}, + {{expiry}}, {{commitment_months}}. + Blocs conditionnels : {{#expiry}}...{{/expiry}}. + { + const blank = ` + +
+ + +
+

Bonjour {{firstname}},

+

Ton message ici…

+
+
+` + html.value = blank + if (editor) editor.setComponents(blank) + dirty.value = true + blankCanvas.value = true + }) +} + +// Force-reload the on-disk version of the current template, discarding any +// unsaved edits. Useful after experimenting in the canvas to get back to +// known-good state without leaving the editor. +async function reloadFromDisk () { + if (dirty.value) { + const ok = await new Promise(resolve => { + $q.dialog({ + title: 'Réinitialiser ?', + message: 'Les changements non sauvegardés seront perdus.', + cancel: true, persistent: true, + }).onOk(() => resolve(true)).onCancel(() => resolve(false)) + }) + if (!ok) return + } + await loadTemplate(currentName.value) + blankCanvas.value = false } onMounted(async () => { diff --git a/services/targo-hub/lib/campaigns.js b/services/targo-hub/lib/campaigns.js index ef7d3fb..3c64640 100644 --- a/services/targo-hub/lib/campaigns.js +++ b/services/targo-hub/lib/campaigns.js @@ -188,10 +188,47 @@ function parseMapCsv (text, multi = 'first') { } // ── Giftbit CSV parsing ────────────────────────────────────────────────────── +// Two supported formats: +// +// 1. "Link Order" export (headerless, one URL per line): +// http://gft.link/4kpZMApLK4B +// http://gft.link/Dn2cb27xYJ8 +// +// This is what Giftbit ships when you pre-buy N gift links upfront without +// targeting specific recipients. Each line = one redeemable shortlink. The +// recipient mapping is done entirely on our side (Map CSV ↔ link by row). +// +// 2. "Campaign" export (with header row, multiple columns): +// firstname,lastname,email,gift_url,giftbit_uuid,gift_value_cents,internal_id +// Alice,Tremblay,alice@x.com,https://...,uuid-001,5000,ACC-1 +// +// This is what create_giftbit_campaign.js produces (and what Giftbit +// sends when you use their Campaign API with delivery_type=SHORTLINK). function parseGiftbitCsv (text) { + const cleaned = text.replace(/^/, '').trim() + if (!cleaned) return [] + + // Detect headerless one-URL-per-line format: first non-empty line starts + // with http:// or https:// and has no comma/pipe/tab separators. + const firstLine = cleaned.split(/\r?\n/, 1)[0].trim() + if (/^https?:\/\/\S+$/.test(firstLine) && !firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|')) { + return cleaned.split(/\r?\n/) + .map(l => l.trim()) + .filter(l => /^https?:\/\//.test(l)) + .map(url => ({ + gift_url: url, + giftbit_uuid: '', + gift_value_cents: 0, + email: '', + firstname: '', + lastname: '', + internal_id: '', + })) + } + + // Otherwise treat as CSV with header row const rows = parseCsv(text) if (!rows.length) return [] - // Detect URL column from common names const keys = Object.keys(rows[0]) const urlCandidates = ['gift_url','gift link','gift_link','url','link','shortlink', 'redemption_url','gift','giftbit_link','campaign_link'] @@ -200,7 +237,6 @@ function parseGiftbitCsv (text) { const hit = keys.find(k => k.toLowerCase().replace(/\s+/g, '_') === c) if (hit) { urlCol = hit; break } } - // Fallback: any column with http(s):// if (!urlCol) { for (const k of keys) { if ((rows[0][k] || '').match(/^https?:\/\//)) { urlCol = k; break }