+
@@ -427,7 +428,7 @@
-
@@ -485,6 +486,19 @@ const reminderPreviewLoading = ref(false)
const showReminderSample = ref(false)
const sendingReminder = ref(false)
const reminderBadgeLabel = 'Petit rappel / Reminder'
+// Cases à cocher par segment — clés = statuts d'origine ('sent'=pas ouvert, 'opened'=ouvert pas cliqué,
+// 'clicked'=cliqué pas inscrit). Toutes cochées par défaut (comportement d'origine = cibler tout le monde).
+const reminderSegments = ref({ sent: true, opened: true, clicked: true })
+const reminderTargetCount = computed(() => {
+ const b = reminderPreview.value?.breakdown; if (!b) return 0
+ const s = reminderSegments.value
+ return (s.sent ? b.not_opened : 0) + (s.opened ? b.opened_not_clicked : 0) + (s.clicked ? b.clicked_not_registered : 0)
+})
+const reminderFilteredSample = computed(() => {
+ const sample = reminderPreview.value?.sample || []
+ const s = reminderSegments.value
+ return sample.filter(x => s[x.status])
+})
const templates = ref([{ value: '', label: 'Invitation Fête 20 ans (TARGO)' }])
const audienceSource = ref('list')
const audFilters = ref({ statut: 'active', customer_type: '', city: '', name_like: '', active_sub: false, min_monthly: 0 })
@@ -884,14 +898,16 @@ async function loadEventCampaigns () {
async function loadReminderPreview () {
if (!reminderCampaignId.value) { reminderPreview.value = null; return }
reminderPreviewLoading.value = true; showReminderSample.value = false
+ reminderSegments.value = { sent: true, opened: true, clicked: true } // repart de « tout le monde » à chaque nouvel envoi source choisi
try { reminderPreview.value = await previewReminder(eventId.value, reminderCampaignId.value) }
catch (e) { $q.notify({ type: 'negative', message: e.message }); reminderPreview.value = null }
finally { reminderPreviewLoading.value = false }
}
function confirmReminderSend () {
- const n = reminderPreview.value?.count || 0
+ const n = reminderTargetCount.value
if (!n) return
const chan = sendChannel.value === 'gmail' ? 'Gmail' : 'Mailjet'
+ const segments = Object.entries(reminderSegments.value).filter(([, checked]) => checked).map(([key]) => key)
$q.dialog({
title: 'Envoyer le rappel',
message: `Envoyer un « Petit rappel » à
${n} destinataire(s) qui n'ont pas encore répondu, via
${chan} ?
Cette action est irréversible.`,
@@ -902,7 +918,7 @@ function confirmReminderSend () {
}).onOk(async () => {
sendingReminder.value = true
try {
- const r = await sendReminder(eventId.value, { campaign_id: reminderCampaignId.value, channel: sendChannel.value })
+ const r = await sendReminder(eventId.value, { campaign_id: reminderCampaignId.value, channel: sendChannel.value, segments })
$q.notify({ type: 'positive', message: `Rappel lancé à ${r.count} destinataire(s)`, caption: 'Suivi dans Campagnes', icon: 'notifications', timeout: 7000, actions: [{ label: 'Ouvrir Campagnes', color: 'white', handler: goCampaigns }] })
showSend.value = false
} catch (e) { $q.notify({ type: 'negative', message: 'Échec du lancement : ' + e.message }) } finally { sendingReminder.value = false }
diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js
index 9d1f6e1..7729c15 100644
--- a/services/targo-hub/lib/events.js
+++ b/services/targo-hub/lib/events.js
@@ -572,7 +572,12 @@ function listEventCampaigns (eventId) {
return campaigns.listCampaigns()
.filter(c => c.event_id === eventId && c.type === 'event' && !c.reminder_of) // exclut les relances (on relance TOUJOURS depuis l'envoi original)
}
-function reminderCandidates (eventId, sourceCampaignId) {
+// `segments` (optionnel) restreint la CIBLE aux statuts d'origine choisis par le staff (case à cocher
+// par catégorie) : 'sent' (pas ouvert), 'opened' (ouvert, pas cliqué), 'clicked' (cliqué, pas inscrit).
+// `breakdown` reflète TOUJOURS le total NON filtré (pour afficher le compte de chaque case, qu'elle
+// soit cochée ou non) ; `candidates`/`count` reflètent le filtre appliqué (la cible réelle de l'envoi).
+const REMINDER_SEGMENTS = ['sent', 'opened', 'clicked']
+function reminderCandidates (eventId, sourceCampaignId, { segments } = {}) {
const campaigns = require('./campaigns')
const src = campaigns.loadCampaign(sourceCampaignId)
if (!src) return { error: 'campaign_not_found' }
@@ -583,28 +588,30 @@ function reminderCandidates (eventId, sourceCampaignId) {
if (r.email) registeredEmails.add(String(r.email).toLowerCase())
if (r.customer_id) registeredCustomers.add(r.customer_id)
}
- const eligible = (src.recipients || []).filter(r => {
+ const fullEligible = (src.recipients || []).filter(r => {
if (r.excluded) return false
- if (!['sent', 'opened', 'clicked'].includes(r.status)) return false // seulement les invitations réellement livrées (pas pending/failed)
+ if (!REMINDER_SEGMENTS.includes(r.status)) return false // seulement les invitations réellement livrées (pas pending/failed)
const email = String(r.email || '').toLowerCase()
if (email && registeredEmails.has(email)) return false
if (r.customer_id && registeredCustomers.has(r.customer_id)) return false
return true
})
const breakdown = {
- not_opened: eligible.filter(r => r.status === 'sent').length,
- opened_not_clicked: eligible.filter(r => r.status === 'opened').length,
- clicked_not_registered: eligible.filter(r => r.status === 'clicked').length,
+ not_opened: fullEligible.filter(r => r.status === 'sent').length,
+ opened_not_clicked: fullEligible.filter(r => r.status === 'opened').length,
+ clicked_not_registered: fullEligible.filter(r => r.status === 'clicked').length,
}
- return { source: { id: src.id, name: src.name, total: (src.recipients || []).length }, candidates: eligible, count: eligible.length, breakdown }
+ const wanted = Array.isArray(segments) && segments.length ? new Set(segments.filter(s => REMINDER_SEGMENTS.includes(s))) : null
+ const candidates = wanted ? fullEligible.filter(r => wanted.has(r.status)) : fullEligible
+ return { source: { id: src.id, name: src.name, total: (src.recipients || []).length }, candidates, count: candidates.length, breakdown }
}
// Crée + lance une campagne de RELANCE (statut sending direct, comme massSend) ciblant
// reminderCandidates() ; le worker (sendEventCampaignAsync) reçoit isReminder:true → badge
// « Petit rappel »/« Reminder » + sujet préfixé. Recalcule les candidats à l'envoi (fraîcheur :
// quelqu'un a pu s'inscrire entre l'aperçu et le clic sur « Envoyer »).
-function reminderSend (eventId, sourceCampaignId, { channel = 'mailjet', from = '' } = {}) {
+function reminderSend (eventId, sourceCampaignId, { channel = 'mailjet', from = '', segments } = {}) {
const event = getEvent(eventId); if (!event) return { error: 'event_not_found' }
- const r = reminderCandidates(eventId, sourceCampaignId)
+ const r = reminderCandidates(eventId, sourceCampaignId, { segments })
if (r.error) return r
if (!r.count) return { error: 'no_candidates' }
const campaigns = require('./campaigns')
@@ -1209,7 +1216,9 @@ async function handle (req, res, method, path, url) {
if (!getEvent(mm[1])) return json(res, 404, { error: 'event_not_found' })
const campaignId = url.searchParams.get('campaign') || ''
if (!campaignId) return json(res, 400, { error: 'campaign_required' })
- const r = reminderCandidates(mm[1], campaignId)
+ const segParam = url.searchParams.get('segments')
+ const segments = segParam ? segParam.split(',').map(s => s.trim()).filter(Boolean) : undefined
+ const r = reminderCandidates(mm[1], campaignId, { segments })
if (r.error) return json(res, 400, { error: r.error })
return json(res, 200, { source: r.source, count: r.count, breakdown: r.breakdown, sample: r.candidates.slice(0, 50).map(x => ({ email: x.email, name: ((x.firstname || '') + ' ' + (x.lastname || '')).trim() || x.email, language: x.language, status: x.status, opened_at: x.opened_at || null, clicked_at: x.clicked_at || null })) })
}
@@ -1229,7 +1238,8 @@ async function handle (req, res, method, path, url) {
const b = await parseBody(req)
if (!b.campaign_id) return json(res, 400, { error: 'campaign_id_required' })
const channel = b.channel === 'gmail' ? 'gmail' : 'mailjet'
- const r = reminderSend(eventId, b.campaign_id, { channel, from: b.from })
+ const segments = Array.isArray(b.segments) ? b.segments : undefined
+ const r = reminderSend(eventId, b.campaign_id, { channel, from: b.from, segments })
if (r.error) return json(res, 400, { error: r.error })
log(`Event REMINDER — ${eventId} · ${channel} · ${r.count} destinataire(s) → campagne ${r.campaign_id} (source ${b.campaign_id})`)
return json(res, 202, { ok: true, campaign_id: r.campaign_id, count: r.count })