diff --git a/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue b/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue
index c87d279..8037055 100644
--- a/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue
+++ b/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue
@@ -3,6 +3,14 @@
Nouvelle campagne
+
+
+
+ S'ouvre dans un nouvel onglet — tes fichiers uploadés restent intacts ici
+
@@ -212,10 +220,27 @@
+
-
+
+
+ Voir le rendu du courriel avec les vraies données du destinataire #1 (n'envoie rien)
+
+
+ Ouvre l'éditeur dans un nouvel onglet — ton import reste ici intact
+
+
+ Va à l'étape de confirmation finale. L'envoi ne démarre qu'au clic sur "Lancer l'envoi" de l'étape 3.
+
@@ -236,19 +261,52 @@
Durée estimée≈ {{ estimatedMinutes }} min
-
- L'envoi démarre dès que vous cliquez ci-dessous.
- Vous serez redirigé vers la page de progression en temps réel.
+
+ Confirmation finale.
+ L'envoi démarre dès le clic sur "Lancer l'envoi maintenant".
+ Tu seras redirigé vers la page de progression temps réel.
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Aperçu du courriel
+
+ · destinataire #{{ previewRecipient.row_index }} {{ previewRecipient.firstname }} {{ previewRecipient.lastname }}
+
+
+
+
+ Éditer dans un nouvel onglet
+
+
+
+
+ Rendu en cours…
+
+
+
+
+
+
@@ -256,7 +314,7 @@
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
-import { parseCsvs, createCampaign, sendCampaign } from 'src/api/campaigns'
+import { parseCsvs, createCampaign, sendCampaign, previewTemplate } from 'src/api/campaigns'
const $q = useQuasar()
const router = useRouter()
@@ -344,6 +402,81 @@ function shortenUrl (u) {
return u.replace(/^https?:\/\//, '').slice(0, 28) + (u.length > 35 ? '…' : '')
}
+// ── Preview dialog ──────────────────────────────────────────────────────────
+// Renders the email through the hub's /campaigns/templates/:name/preview
+// endpoint, using the first sendable recipient's data + the current campaign
+// params. Non-destructive — no emails are fired by this action.
+const previewOpen = ref(false)
+const previewLoading = ref(false)
+const previewHtmlContent = ref('')
+const previewLang = ref('fr')
+const previewRecipient = ref(null)
+
+// First recipient that's actually going to be sent — used as the preview
+// sample so the user sees real data, not synthetic placeholders.
+const firstPreviewable = computed(() =>
+ recipients.value.find(r => !r.excluded && r.gift_url) || null
+)
+
+// Link to the template editor for the relevant language. Always opens
+// in a new tab so the user's in-progress wizard state is preserved.
+const editorHref = computed(() =>
+ `/ops/#/campaigns/templates/gift-email-${previewLang.value}`
+)
+
+async function openPreview () {
+ const r = firstPreviewable.value
+ if (!r) return
+ previewRecipient.value = r
+ // Default preview language to the recipient's actual language so the user
+ // first sees what THIS recipient will receive
+ previewLang.value = (r.language || 'fr').toLowerCase().split('-')[0]
+ previewOpen.value = true
+ await renderPreview()
+}
+
+async function renderPreview () {
+ if (!previewRecipient.value) return
+ previewLoading.value = true
+ try {
+ const r = previewRecipient.value
+ const vars = {
+ firstname: r.firstname || (previewLang.value === 'en' ? 'dear customer' : 'cher client'),
+ lastname: r.lastname || '',
+ email: r.email,
+ description: r.civic_address || '',
+ gift_url: r.gift_url,
+ amount: params.value.amount,
+ expiry: params.value.expiry,
+ commitment_months: params.value.commitment_months,
+ year: new Date().getFullYear(),
+ }
+ const res = await previewTemplate(`gift-email-${previewLang.value}`, { vars })
+ previewHtmlContent.value = res.rendered || ''
+ } catch (e) {
+ $q.notify({ type: 'negative', message: 'Aperçu impossible: ' + e.message })
+ } finally {
+ previewLoading.value = false
+ }
+}
+
+// Wrapper around launchSend that confirms one last time before firing. The
+// Step 3 page is already a "confirmation step", but this dialog adds one
+// final friction so accidental clicks don't fire 200 emails.
+function confirmAndLaunch () {
+ $q.dialog({
+ title: 'Envoyer maintenant ?',
+ message: `Cette action enverra ${sendableCount.value} courriel(s)
+ via Mailjet immédiatement. Pas annulable une fois démarré.`,
+ html: true,
+ persistent: true,
+ ok: { label: 'Oui, envoyer', color: 'negative', icon: 'send', unelevated: true },
+ cancel: { label: 'Annuler', flat: true },
+ }).onOk(() => {
+ launchSend()
+ })
+}
+
function readFile (file) {
return new Promise((resolve, reject) => {
const r = new FileReader()