diff --git a/apps/ops/src/pages/EventsPage.vue b/apps/ops/src/pages/EventsPage.vue index 82d1366..b63735c 100644 --- a/apps/ops/src/pages/EventsPage.vue +++ b/apps/ops/src/pages/EventsPage.vue @@ -142,11 +142,11 @@ - - + + -
« Total mensuel » = somme des abonnements actifs du client (0 = aucun filtre). « Résidentiel/Commercial » = type de compte.
+
Le « total mensuel » (somme des abonnements actifs) requiert d'activer « Abonnement actif ». « Résidentiel/Commercial » = type de compte.
@@ -163,6 +163,18 @@
ex. {{ audPreview.sample.slice(0, 4).map(s => s.firstname || s.email).join(', ') }}…
+
+ + {{ showNoEmail ? '▾ Masquer' : '▸ Voir' }} les {{ audPreview.dropped_no_email }} comptes sans courriel (à valider) + +
+
+ {{ ne.name }} + · {{ ne.customer_id }} +
+
…et {{ audPreview.dropped_no_email - audPreview.no_email.length }} autres (liste plafonnée à 300)
+
+
@@ -220,6 +232,7 @@ const csvFile = ref(null) const audCsvText = ref('') const audPreview = ref(null) const previewing = ref(false) +const showNoEmail = ref(false) const publicUrl = computed(() => ev.value.public_url || '') @@ -301,8 +314,9 @@ function onCsvFile (file) { rd.onload = () => { audCsvText.value = String(rd.result || '') } rd.readAsText(file) } +function onActiveSubToggle (v) { audPreview.value = null; if (!v) audFilters.value.min_monthly = 0 } // le montant n'a de sens qu'avec les abonnements actifs async function doPreview () { - previewing.value = true; audPreview.value = null + previewing.value = true; audPreview.value = null; showNoEmail.value = false try { const body = audienceSource.value === 'csv' ? { mode: 'csv', csv: audCsvText.value } : { mode: 'filter', filters: audFilters.value } audPreview.value = await previewAudience(EVENT_ID, body) diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js index 491c0ca..48e35ed 100644 --- a/services/targo-hub/lib/events.js +++ b/services/targo-hub/lib/events.js @@ -294,13 +294,17 @@ async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {} } rows = customers.map(c => { const n = splitName(c.customer_name); return { email: firstEmail(c.email_id), firstname: n.firstname, lastname: n.lastname, language: normLang(c.language), customer_id: c.name, customer_type: c.customer_type || '', monthly: Math.round(c._monthly || 0) } }) } - const seen = new Set(); const recipients = []; let dropped = 0 + const seen = new Set(); const recipients = []; const noEmail = []; let dropped = 0 for (const r of rows) { - if (!EMAIL_RE.test(r.email || '')) { dropped++; continue } + if (!EMAIL_RE.test(r.email || '')) { + dropped++ + if (r.customer_id) noEmail.push({ customer_id: r.customer_id, name: ((r.firstname || '') + ' ' + (r.lastname || '')).trim() || r.customer_id }) // rattachable → lien fiche pour valider + continue + } const k = r.email.toLowerCase(); if (seen.has(k)) continue; seen.add(k) recipients.push(r) } - return { recipients, count: recipients.length, dropped_no_email: dropped } + return { recipients, count: recipients.length, dropped_no_email: dropped, no_email: noEmail } } // ── Page publique (festive, mobile-first, bilingue) ──────────────────────── @@ -565,7 +569,7 @@ async function handle (req, res, method, path, url) { const b = await parseBody(req) const r = await resolveAudience({ mode: b.mode, filters: b.filters || {}, csv: b.csv || '' }) if (r.error) return json(res, 400, { error: r.error }) - return json(res, 200, { count: r.count, dropped_no_email: r.dropped_no_email, sample: r.recipients.slice(0, 50) }) + return json(res, 200, { count: r.count, dropped_no_email: r.dropped_no_email, sample: r.recipients.slice(0, 50), no_email: (r.no_email || []).slice(0, 300) }) } return json(res, 404, { error: 'not found' })