From e64e1e6a1fd014901dfe184d290baea1160aa137 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 1 Jun 2026 11:51:45 -0400 Subject: [PATCH] feat(campaigns/detail): edit draft params + jump to template editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new buttons on the campaign detail page header — both visible only when campaign.status === 'draft' to keep operators from accidentally mutating a campaign mid-send. "Éditer les paramètres" → q-dialog with: - name (internal) - subject (the email Subject: line) - from (sender) - amount displayed in the body (overrides per-recipient default) - commitment_months - expiry text - template_fr / template_en dropdowns (refresh on popup-show so newly created templates show up without a page reload) Saves via the existing PATCH /campaigns/:id, which merges into params. A live load() refresh updates the Confirmation recap and any visible counters. "Éditer le template" → opens the Unlayer editor in a new tab on the campaign's configured template_fr (most TARGO customers FR). For campaign-specific tweaks the dialog tells the operator to create a variant template (+ Nouveau) and select it here. Addresses the gap a user hit on a reminder draft — they wanted to add a condition to the body before launching but had no edit affordance on the detail page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 --- .../campaigns/pages/CampaignDetailPage.vue | 170 +++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue b/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue index 79e236c..b66e81a 100644 --- a/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue +++ b/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue @@ -17,6 +17,19 @@ :loading="creatingReminder" @click="confirmCreateReminder"> Cloner cette campagne pour les destinataires qui n'ont PAS cliqué le cadeau encore + + + Modifier le contenu HTML du template dans un nouvel onglet (Unlayer) + + + Sujet, expéditeur, montant affiché, choix de template (FR/EN), etc. + @@ -125,6 +138,59 @@
Campagne introuvable
+ + + + + +
Éditer les paramètres
+ + +
+ + + + + +
+ + + +
+
+ + +
+
+ Pour modifier le contenu du courriel + (texte, mise en page, conditions, etc.), clique "Éditer le template" + à côté — ça ouvre l'éditeur Unlayer dans un nouvel onglet. Les modifications de template + s'appliquent à toutes les campagnes qui utilisent ce template ; pour + une version unique à cette campagne, crée d'abord un template variant + (+ Nouveau dans l'éditeur) et sélectionne-le ici. +
+
+ + + +
+
+
+
+
@@ -132,7 +198,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useQuasar } from 'quasar' -import { getCampaign, sendCampaign, campaignSseUrl, campaignReportCsvUrl, retryRecipient, createReminderCampaign } from 'src/api/campaigns' +import { getCampaign, sendCampaign, campaignSseUrl, campaignReportCsvUrl, retryRecipient, createReminderCampaign, updateCampaign, listTemplates } from 'src/api/campaigns' const route = useRoute() const router = useRouter() @@ -143,6 +209,53 @@ const loading = ref(true) const resending = ref(false) const creatingReminder = ref(false) +// Edit-params dialog state. editParams is a snapshot of campaign.params +// that the form mutates; on save we PATCH only that subset back to the +// hub, which merges over the existing params on disk. +const editParamsOpen = ref(false) +const savingParams = ref(false) +const editParams = ref({ + name: '', subject: '', from: '', amount: '', commitment_months: 3, + expiry: '', template_fr: 'gift-email-fr', template_en: 'gift-email-en', +}) + +// Template lists for the FR/EN dropdowns in the dialog. Refreshed lazily +// when the dropdown opens so newly-created templates show up without a +// page reload. +const loadingTemplates = ref(false) +const frTemplateOptions = ref([{ label: 'gift-email-fr', value: 'gift-email-fr' }]) +const enTemplateOptions = ref([{ label: 'gift-email-en', value: 'gift-email-en' }]) +async function loadTemplateLists () { + loadingTemplates.value = true + try { + const tpls = await listTemplates() + const opt = (t, preferred) => ({ + label: preferred ? t.name : `${t.name} · sans suffixe de langue`, + value: t.name, + }) + const fr = [], en = [] + for (const t of tpls) { + if (!t?.name) continue + if (t.name.endsWith('-fr')) fr.push(opt(t, true)) + else if (t.name.endsWith('-en')) en.push(opt(t, true)) + else { fr.push(opt(t, false)); en.push(opt(t, false)) } + } + const sortFn = (a, b) => { + const aPref = !a.label.includes('sans suffixe') + const bPref = !b.label.includes('sans suffixe') + if (aPref !== bPref) return aPref ? -1 : 1 + return a.value.localeCompare(b.value) + } + fr.sort(sortFn); en.sort(sortFn) + if (fr.length) frTemplateOptions.value = fr + if (en.length) enTemplateOptions.value = en + } catch (e) { + $q.notify({ type: 'warning', message: 'Templates non chargés : ' + e.message }) + } finally { + loadingTemplates.value = false + } +} + let es = null const columns = [ @@ -185,6 +298,14 @@ function giftbitAdminUrl (giftUrl) { const reportCsvUrl = computed(() => campaignReportCsvUrl(id)) +// Deep link to the template editor for the FR template configured on +// this campaign (most TARGO customers are FR). The editor route also +// accepts no name and lets the user pick from a dropdown. +const templateEditorHref = computed(() => { + const t = campaign.value?.params?.template_fr || 'gift-email-fr' + return `/ops/#/campaigns/templates/${encodeURIComponent(t)}` +}) + // Eligibility for the "Créer une relance" button: campaign sent at least // once (not draft), it's not itself a reminder, and at least one recipient // is in a non-clicked / non-revoked / non-expired state. The hub re-checks @@ -283,6 +404,53 @@ async function retryFailedRow (row) { } } +// Open the params dialog, snapshotting current campaign.params + name +// into the editable form. Lazily load the template lists so the +// dropdowns are populated when the dialog mounts. +function openEditParams () { + const p = campaign.value?.params || {} + editParams.value = { + name: campaign.value?.name || '', + subject: p.subject || '', + from: p.from || '', + amount: p.amount || '', + commitment_months: p.commitment_months || 3, + expiry: p.expiry || '', + template_fr: p.template_fr || 'gift-email-fr', + template_en: p.template_en || 'gift-email-en', + } + editParamsOpen.value = true + loadTemplateLists() +} + +// PATCH the campaign with the new name + params. Hub merges params over +// the existing ones (spread) so unspecified fields stay put. Live UI +// refresh via load() so the recap + counters reflect the new values. +async function saveEditParams () { + savingParams.value = true + try { + await updateCampaign(id, { + name: editParams.value.name, + params: { + subject: editParams.value.subject, + from: editParams.value.from, + amount: editParams.value.amount, + commitment_months: editParams.value.commitment_months, + expiry: editParams.value.expiry, + template_fr: editParams.value.template_fr, + template_en: editParams.value.template_en, + }, + }) + await load() + editParamsOpen.value = false + $q.notify({ type: 'positive', message: 'Paramètres enregistrés' }) + } catch (e) { + $q.notify({ type: 'negative', message: 'Erreur : ' + e.message }) + } finally { + savingParams.value = false + } +} + // Confirmation + creation of the reminder campaign. The hub returns the // new draft campaign; we navigate to its detail page so the operator can // review the recipients list and click "Lancer l'envoi" when ready.