From 5d780187b0b10a838ff6e8959a135b848c7b58b6 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 6 Jul 2026 14:25:05 -0400 Subject: [PATCH] =?UTF-8?q?feat(events):=20filtres=20audience=20pr=C3=A9ci?= =?UTF-8?q?s=20=E2=80=94=20type=20(r=C3=A9sidentiel/commercial)=20+=20tota?= =?UTF-8?q?l=20mensuel=20min?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout au sélecteur d'audience (mode masse) : - Type de client : Résidentiel (customer_type=Individual) / Commercial (Company) / Tous. - Total mensuel minimum ($/mois) : somme des Service Subscription ACTIFS (monthly_price) par client ≥ seuil. resolveAudience agrège les abonnements actifs quand active_sub OU min_monthly>0 ; le total est renvoyé par destinataire (monthly). Vérifié sur données prod réelles : Résidentiel 7099, Commercial 928, total mensuel ≥50$ = 2739 clients (montants agrégés réels). Champs bien peuplés (contrairement à territoire, retiré). 32/32 tests unitaires. Déployé hub + SPA. Le blast réel reste gaté. Co-Authored-By: Claude Opus 4.8 --- apps/ops/src/pages/EventsPage.vue | 12 +++++++++--- services/targo-hub/lib/events.js | 20 +++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/ops/src/pages/EventsPage.vue b/apps/ops/src/pages/EventsPage.vue index fdb1a26..82d1366 100644 --- a/apps/ops/src/pages/EventsPage.vue +++ b/apps/ops/src/pages/EventsPage.vue @@ -138,9 +138,15 @@ :options="[{ label: 'Liste clients', value: 'list' }, { label: 'Import CSV', value: 'csv' }]" />
- +
+ + +
-
Astuce : « Clients actifs » = ~7900 clients avec courriel. « Abonnement actif » restreint à ceux ayant un service en cours.
+ + + +
« Total mensuel » = somme des abonnements actifs du client (0 = aucun filtre). « Résidentiel/Commercial » = type de compte.
@@ -209,7 +215,7 @@ const sendMode = ref('test') const template = ref('') const templates = ref([{ value: '', label: 'Invitation Fête 20 ans (TARGO)' }]) const audienceSource = ref('list') -const audFilters = ref({ statut: 'active', active_sub: false }) +const audFilters = ref({ statut: 'active', customer_type: '', active_sub: false, min_monthly: 0 }) const csvFile = ref(null) const audCsvText = ref('') const audPreview = ref(null) diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js index fc322d4..491c0ca 100644 --- a/services/targo-hub/lib/events.js +++ b/services/targo-hub/lib/events.js @@ -273,24 +273,26 @@ async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {} const f = [] if (filters.statut === 'active') f.push(['disabled', '=', 0]) else if (filters.statut === 'disabled') f.push(['disabled', '=', 1]) - if (filters.territory) f.push(['territory', '=', filters.territory]) - if (filters.group) f.push(['customer_group', '=', filters.group]) - const CFIELDS = ['name', 'customer_name', 'email_id', 'language', 'territory', 'customer_group'] + if (filters.customer_type) f.push(['customer_type', '=', filters.customer_type]) // Individual = résidentiel · Company = commercial + const CFIELDS = ['name', 'customer_name', 'email_id', 'language', 'customer_type'] + const minMonthly = Number(filters.min_monthly) || 0 let customers = [] - if (filters.active_sub) { - // Clients ayant ≥1 Service Subscription « Actif ». - const subs = []; let s = 0 - for (let p = 0; p < 30; p++) { const b = await erp.list('Service Subscription', { filters: [['status', '=', 'Actif']], fields: ['customer'], limit: 500, start: s }); subs.push(...b); if (b.length < 500) break; s += 500 } - const ids = [...new Set(subs.map(x => x.customer).filter(Boolean))] + if (filters.active_sub || minMonthly > 0) { + // Agrège les abonnements ACTIFS par client (somme mensuelle) ; filtre par minimum si demandé. + const byCust = {}; let s = 0 + for (let p = 0; p < 40; p++) { const b = await erp.list('Service Subscription', { filters: [['status', '=', 'Actif']], fields: ['customer', 'monthly_price'], limit: 500, start: s }); b.forEach(x => { if (x.customer) byCust[x.customer] = (byCust[x.customer] || 0) + (Number(x.monthly_price) || 0) }); if (b.length < 500) break; s += 500 } + let ids = Object.keys(byCust) + if (minMonthly > 0) ids = ids.filter(c => byCust[c] >= minMonthly) for (let i = 0; i < ids.length; i += 100) { const b = await erp.list('Customer', { filters: [...f, ['name', 'in', ids.slice(i, i + 100)]], fields: CFIELDS, limit: 500 }) customers.push(...b) } + customers.forEach(c => { c._monthly = byCust[c.name] || 0 }) } else { let s = 0 for (let p = 0; p < 60; p++) { const b = await erp.list('Customer', { filters: f, fields: CFIELDS, limit: 500, start: s }); customers.push(...b); if (b.length < 500) break; s += 500 } } - 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 } }) + 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 for (const r of rows) {