diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js index 4ded31b..2127ca5 100644 --- a/services/targo-hub/lib/events.js +++ b/services/targo-hub/lib/events.js @@ -507,6 +507,32 @@ async function cityCustomerIds (city) { for (let p = 0; p < 80; p++) { const b = await erp.list('Service Location', { filters: [['city', 'like', '%' + term + '%']], fields: ['customer'], limit: 500, start: s }); b.forEach(x => { if (x.customer) ids.add(x.customer) }); if (b.length < 500) break; s += 500 } return ids } + +// Filtre « nom » de l'audience — FUZZY comme la recherche app-wide : accent/casse-insensible ET multi-champs +// (customer_name + ID de compte `name` + mandataire + contact_name_legacy). Ex. « lpb » → C-LPB4 (Louis-Paul +// Bourdon, matché par ID/mandataire, pas par customer_name). Via PG ; repli REST (customer_name, sensible à la casse). +async function nameLikeCustomerIds (term) { + const t = String(term || '').trim(); const ids = new Set() + if (!t) return ids + let pool = null + try { pool = require('./address-db').pool() } catch { pool = null } + if (pool) { + const like = '%' + t + '%' + try { + const r = await pool.query("SELECT name FROM \"tabCustomer\" WHERE unaccent(lower(coalesce(customer_name,''))) LIKE unaccent(lower($1)) OR lower(coalesce(name,'')) LIKE lower($1)", [like]) + r.rows.forEach(x => { if (x.name) ids.add(x.name) }) + } catch (e) { log('nameLikeCustomerIds pg: ' + e.message) } + // Champs legacy (colonnes custom possibles) — try/catch séparé pour ne pas casser si absentes. + try { + const r = await pool.query("SELECT name FROM \"tabCustomer\" WHERE unaccent(lower(coalesce(mandataire,''))) LIKE unaccent(lower($1)) OR unaccent(lower(coalesce(contact_name_legacy,''))) LIKE unaccent(lower($1))", [like]) + r.rows.forEach(x => { if (x.name) ids.add(x.name) }) + } catch { /* colonnes absentes → ignore */ } + return ids + } + const erp = require('./erp'); let s = 0 + for (let p = 0; p < 80; p++) { const b = await erp.list('Customer', { filters: [['customer_name', 'like', '%' + t + '%']], fields: ['name'], limit: 500, start: s }); b.forEach(x => { if (x.name) ids.add(x.name) }); if (b.length < 500) break; s += 500 } + return ids +} async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {}) { let rows = [] let resiliatedExcluded = 0 @@ -529,13 +555,14 @@ async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {} if (filters.statut === 'active') f.push(['disabled', '=', 0]) else if (filters.statut === 'disabled') f.push(['disabled', '=', 1]) if (filters.customer_type) f.push(['customer_type', '=', filters.customer_type]) // Individual = résidentiel · Company = commercial - if (filters.name_like) f.push(['customer_name', 'like', '%' + String(filters.name_like).trim() + '%']) // ex. « Proprio » → comptes dont le nom le contient const minMonthly = Number(filters.min_monthly) || 0 - // Ensembles d'IDs à INTERSECTER (ville via Service Location, abonnements via Service Subscription). + // Ensembles d'IDs à INTERSECTER (nom fuzzy, ville via Service Location, abonnements via Service Subscription). let restrict = null // null = pas de restriction ; sinon Set d'IDs Customer const intersect = (set) => { restrict = restrict ? new Set([...restrict].filter(x => set.has(x))) : set } + // NOM (fuzzy, multi-champs, insensible casse/accents) — ex. « lpb » → C-LPB4. + if (filters.name_like && String(filters.name_like).trim()) intersect(await nameLikeCustomerIds(String(filters.name_like))) // VILLE → Service Location.customer (champ `city` peuplé, contrairement à `territory`). if (filters.city && String(filters.city).trim()) intersect(await cityCustomerIds(String(filters.city))) @@ -629,7 +656,6 @@ function page (event, lang, prefill = {}, opts = {}) { lang = normLang(lang) const t = event[lang] const other = lang === 'fr' ? 'en' : 'fr' - const known = !!prefill.customer_id const full = !!opts.full const greet = t.greet(firstName(prefill.name)) const program = (t.program || []).map(p => @@ -726,7 +752,6 @@ button:hover{filter:brightness(1.05)}button:active{transform:translateY(1px)}but ${field('nm', 'name', esc(t.f_name), ``, 'e_nm', t.err_name)} ${field('em', 'email', esc(t.f_email), ``, 'e_em', t.err_email)} ${field('ph', 'phone', optlbl(t.f_phone), ``)} - ${field('cn', 'customer_number', optlbl(t.f_number), ``)} ${field('ps', 'party_size', esc(t.f_party), ``, 'e_ps', t.err_party)} ${field('al', 'allergies', optlbl(t.f_allerg), ``)} @@ -769,7 +794,7 @@ function submitRsvp(e){ if(!ok)return false; var b=document.getElementById('sub');b.disabled=true; fetch('/rsvp/'+encodeURIComponent(EVENT)+'/submit',{method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({token:TOKEN,name:nm,email:em,phone:(f.phone.value||'').trim(),customer_number:(f.customer_number.value||'').trim(),party_size:ps,allergies:(f.allergies.value||'').trim(),lang:LANG})}) + body:JSON.stringify({token:TOKEN,name:nm,email:em,phone:(f.phone.value||'').trim(),party_size:ps,allergies:(f.allergies.value||'').trim(),lang:LANG})}) .then(function(r){return r.json()}) .then(function(d){ if(d&&d.ok){