From 1ab899388c111b8491f5e237ad70e2afdb967845 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 6 Jul 2026 14:38:19 -0400 Subject: [PATCH] =?UTF-8?q?feat(events):=20total=20mensuel=20activ=C3=A9?= =?UTF-8?q?=20seulement=20si=20=C2=AB=20abonnement=20actif=20=C2=BB=20+=20?= =?UTF-8?q?liste=20des=20comptes=20sans=20courriel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Le champ « Total mensuel minimum » est désactivé tant que « Abonnement actif » n'est pas coché (le montant vient des abonnements actifs) ; il se remet à 0 si on décoche. - L'aperçu d'audience liste maintenant les comptes SANS courriel (rattachables) avec un lien vers chaque fiche client (/clients/:id) → permet de valider/ajouter le courriel manquant. resolveAudience renvoie `no_email:[{customer_id,name}]` (plafonné 300) ; l'endpoint le transmet. Vérifié prod : filtre abonnement actif + ≥40$/mois → 3102 destinataires · 284 sans courriel (liste des 284 renvoyée avec noms + fiches). Champ montant désactivé par défaut, activé au toggle. 33/33 tests unitaires. Déployé hub + SPA. Co-Authored-By: Claude Opus 4.8 --- apps/ops/src/pages/EventsPage.vue | 22 ++++++++++++++++++---- services/targo-hub/lib/events.js | 12 ++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) 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' })