From 2b85735006e1537207d4a3bef2369e7ca48a21dc Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 21 May 2026 21:01:40 -0400 Subject: [PATCH] fix(ops/campaigns): clarify Step 2 actions + add inline preview + jump-to-editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User confusion: the "Approuver — 3 à envoyer" button at the end of Step 2 had a send icon, suggesting it fired emails immediately. It actually just navigated to Step 3 (the confirmation step). The current flow has two consent moments (Step 2 approve → Step 3 launch) but the UI made them look like one. Three changes to address this: 1. Step 2 navigation button: - Icon changed from 'send' to 'arrow_forward' — clearly "next step" - Label changed from "Approuver — N à envoyer" to "Continuer — N prêts" - Added tooltip explaining the send only happens at Step 3 2. Inline preview dialog: - New "Aperçu du courriel" button in Step 2 (and Step 3) - Opens a maximized dialog with an iframe rendering the actual template via POST /campaigns/templates/:name/preview, using the first sendable recipient's real data + the campaign params (amount, expiry, etc.) - FR/EN toggle inside the dialog so the user can verify both templates before launching a mixed-language campaign - Defaults to the recipient's own language for first view - Non-destructive — fires zero emails 3. Always-accessible "Éditer le template" link: - Persistent button in the page header (visible all 3 steps) - Plus secondary buttons in Step 2 + Step 3 action rows - Opens the template editor in a NEW TAB so the wizard's state (uploaded CSVs, parsed recipients) stays intact in the original tab — the user can tweak the template, save, switch back, click "Aperçu" to see the change, then continue with the send 4. Step 3 confirmation hardening: - Banner color escalated from amber to red (this IS the point of no return for actual delivery) - Wrap the launch button click in a Quasar confirm dialog ("Envoyer N courriel(s) maintenant ? Pas annulable.") — adds a third friction point against accidental clicks - Launch button is red (negative) — visually distinct from the green navigation primaries to signal "destructive action ahead" - Back-to-Step 2 button renamed "Retour modifier" with arrow_back icon for clarity Co-Authored-By: Claude Opus 4.7 --- .../campaigns/pages/CampaignNewPage.vue | 151 ++++++++++++++++-- 1 file changed, 142 insertions(+), 9 deletions(-) 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()