fix(ops/campaigns): clarify Step 2 actions + add inline preview + jump-to-editor
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 <noreply@anthropic.com>
This commit is contained in:
parent
d6096fe1f8
commit
2b85735006
|
|
@ -3,6 +3,14 @@
|
|||
<div class="row items-center q-mb-md">
|
||||
<q-btn flat dense round icon="arrow_back" :to="'/campaigns'" class="q-mr-sm" />
|
||||
<div class="text-h5">Nouvelle campagne</div>
|
||||
<q-space />
|
||||
<!-- Always-accessible jump-to-editor link. Opens in new tab so the
|
||||
wizard's uploaded files + parsed recipients stay intact. Useful
|
||||
when the user wants to tweak the template mid-import. -->
|
||||
<q-btn flat color="primary" icon="palette" label="Éditer le template (nouvel onglet)"
|
||||
type="a" href="/ops/#/campaigns/templates/gift-email-fr" target="_blank">
|
||||
<q-tooltip>S'ouvre dans un nouvel onglet — tes fichiers uploadés restent intacts ici</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<q-stepper v-model="step" header-nav ref="stepper" color="primary" animated flat bordered class="bg-white">
|
||||
|
|
@ -212,10 +220,27 @@
|
|||
</q-list>
|
||||
</q-expansion-item>
|
||||
|
||||
<!-- Action row: preview + edit template are quick-access utilities,
|
||||
both non-destructive. The primary action is "Continuer" which
|
||||
moves to Step 3 (still NOT the send — Step 3 has its own
|
||||
explicit launch button). Icon changed from 'send' (confusing,
|
||||
looked like it fired) to 'arrow_forward'. -->
|
||||
<q-stepper-navigation>
|
||||
<q-btn flat label="Retour" @click="step = 1" />
|
||||
<q-btn unelevated color="primary" :label="`Approuver — ${sendableCount} à envoyer`" icon-right="send"
|
||||
class="q-ml-sm" @click="step = 3" :disable="sendableCount === 0" />
|
||||
<q-space />
|
||||
<q-btn flat color="primary" icon="visibility" label="Aperçu du courriel"
|
||||
:disable="!firstPreviewable" @click="openPreview" class="q-mr-sm">
|
||||
<q-tooltip>Voir le rendu du courriel avec les vraies données du destinataire #1 (n'envoie rien)</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat color="primary" icon="palette" label="Éditer le template"
|
||||
type="a" :href="editorHref" target="_blank" class="q-mr-sm">
|
||||
<q-tooltip>Ouvre l'éditeur dans un nouvel onglet — ton import reste ici intact</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn unelevated color="primary" :label="`Continuer — ${sendableCount} prêts`"
|
||||
icon-right="arrow_forward"
|
||||
@click="step = 3" :disable="sendableCount === 0">
|
||||
<q-tooltip>Va à l'étape de confirmation finale. L'envoi ne démarre qu'au clic sur "Lancer l'envoi" de l'étape 3.</q-tooltip>
|
||||
</q-btn>
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
|
||||
|
|
@ -236,19 +261,52 @@
|
|||
<q-item><q-item-section side>Durée estimée</q-item-section><q-item-section>≈ {{ estimatedMinutes }} min</q-item-section></q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-card-section class="bg-orange-1 text-orange-9">
|
||||
<q-icon name="info" /> L'envoi démarre dès que vous cliquez ci-dessous.
|
||||
Vous serez redirigé vers la page de progression en temps réel.
|
||||
<q-card-section class="bg-red-1 text-red-9">
|
||||
<q-icon name="warning" /> <strong>Confirmation finale.</strong>
|
||||
L'envoi démarre dès le clic sur <em>"Lancer l'envoi maintenant"</em>.
|
||||
Tu seras redirigé vers la page de progression temps réel.
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-stepper-navigation>
|
||||
<q-btn flat label="Retour" @click="step = 2" />
|
||||
<q-btn unelevated color="primary" label="Lancer l'envoi" icon-right="send"
|
||||
class="q-ml-sm" :loading="sending" @click="launchSend" />
|
||||
<q-btn flat label="Retour modifier" icon="arrow_back" @click="step = 2" />
|
||||
<q-space />
|
||||
<q-btn flat color="primary" icon="visibility" label="Aperçu courriel" :disable="!firstPreviewable" @click="openPreview" class="q-mr-sm" />
|
||||
<q-btn unelevated color="negative" label="Lancer l'envoi maintenant" icon-right="send"
|
||||
:loading="sending" @click="confirmAndLaunch" />
|
||||
</q-stepper-navigation>
|
||||
</q-step>
|
||||
|
||||
</q-stepper>
|
||||
|
||||
<!-- Preview dialog — renders the actual template with the first sendable
|
||||
recipient's data + the campaign params. Lets the user verify the
|
||||
visual + content WITHOUT firing any emails. Toggleable FR/EN since
|
||||
a mixed-language campaign would send both templates. -->
|
||||
<q-dialog v-model="previewOpen" maximized persistent>
|
||||
<q-card class="bg-grey-2">
|
||||
<q-toolbar class="bg-white" style="border-bottom:1px solid #e5e7eb">
|
||||
<q-icon name="visibility" class="q-mr-sm" />
|
||||
<q-toolbar-title>
|
||||
Aperçu du courriel
|
||||
<span v-if="previewRecipient" class="text-caption text-grey-7">
|
||||
· destinataire #{{ previewRecipient.row_index }} {{ previewRecipient.firstname }} {{ previewRecipient.lastname }}
|
||||
</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn-toggle v-model="previewLang" :options="[{label:'🇫🇷 FR', value:'fr'},{label:'🇺🇸 EN', value:'en'}]"
|
||||
dense unelevated toggle-color="primary" @update:model-value="renderPreview" />
|
||||
<q-btn flat icon="open_in_new" :href="editorHref" target="_blank" class="q-mx-sm">
|
||||
<q-tooltip>Éditer dans un nouvel onglet</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat dense round icon="close" @click="previewOpen = false" />
|
||||
</q-toolbar>
|
||||
<q-banner v-if="previewLoading" class="bg-blue-1 text-blue-9">
|
||||
<q-spinner size="sm" /> Rendu en cours…
|
||||
</q-banner>
|
||||
<q-card-section class="q-pa-md" style="height: calc(100vh - 60px); overflow:hidden">
|
||||
<iframe :srcdoc="previewHtmlContent" style="width:100%; height:100%; border:1px solid #e5e7eb; background:#fff;"></iframe>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
|
|
@ -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 <strong>${sendableCount.value} courriel(s)</strong>
|
||||
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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user