diff --git a/services/targo-hub/lib/legacy-dispatch-sync.js b/services/targo-hub/lib/legacy-dispatch-sync.js index 59e7c8d..8d4c300 100644 --- a/services/targo-hub/lib/legacy-dispatch-sync.js +++ b/services/targo-hub/lib/legacy-dispatch-sync.js @@ -2624,27 +2624,60 @@ async function techHistory ({ tech = '', limit = 200 } = {}) { // assignations : garantit une identité canonique portant son legacy_staff_id (+ tech_id SI Dispatch Technician → les // admins restent SANS tech_id). LINK (identité trouvée par courriel/nom → renseigne legacy_staff_id) ou CREATE (aucune → // crée depuis le staff). Idempotent. dryRun => 0 écriture. scope 'ledger' (défaut : staff du ledger) | 'active' (tout staff actif). +// Comptes SYSTÈME du legacy staff (pas des personnes) → jamais d'identité auto. +const SYS_STAFF_RE = /ticket\s*dispatch|gestion\s*inventaire|^tech\s*targo$|^test\b/i +// Index nom→emails depuis les sources EXTERNES (ERPNext Employee + Authentik) — enrichit les staff sans courriel +// legacy (ex-employés hors staff.email) ET alimente les alias multi-courriels (cf 3 sources de noms/identité). +async function extEmailIndex () { + const byName = new Map() // norm(nom complet) → Set(emails) + const add = (name, mail) => { const k = norm(name || ''); const v = String(mail || '').trim().toLowerCase(); if (!k || !v || !v.includes('@')) return; const set = byName.get(k) || new Set(); set.add(v); byName.set(k, set) } + try { + const emps = await erp.list('Employee', { fields: ['employee_name', 'company_email', 'prefered_email', 'personal_email', 'user_id'], limit: 2000 }) + for (const e of (emps || [])) for (const m of [e.company_email, e.prefered_email, e.personal_email, e.user_id]) add(e.employee_name, m) + } catch (e) {} + try { + const d = await akGet('/core/users/?page_size=500') + for (const u of ((d && d.results) || [])) add(u.name, u.email) + } catch (e) {} + return byName +} async function provisionIdentities ({ dryRun = true, scope = 'ledger' } = {}) { const p = pool(); const pg = techHistPg(); const idn = require('./identity') let staffIds = [] if (scope === 'active') { const [r] = await p.query('SELECT id FROM staff WHERE status=1'); staffIds = r.map(x => x.id) } else { const r = await pg.query('SELECT DISTINCT to_staff_id FROM ops_tech_ticket_history WHERE to_staff_id IS NOT NULL'); staffIds = r.rows.map(x => x.to_staff_id) } - if (!staffIds.length) return { ok: true, scope, targets: 0, linked: 0, created: 0, already: 0, no_email: [], samples: [] } + if (!staffIds.length) return { ok: true, scope, targets: 0, linked: 0, created: 0, enriched: 0, already: 0, system: 0, no_email: [], samples: [] } const [staff] = await p.query('SELECT id, first_name, last_name, email, status FROM staff WHERE id IN (?)', [staffIds]) const byId = new Map(staff.map(s => [String(s.id), s])) const techs = await erp.list('Dispatch Technician', { fields: ['name', 'technician_id'], limit: 1000 }) const techByStaff = {}; for (const t of (techs || [])) { const m = String(t.technician_id || '').match(/(\d+)$/); if (m) techByStaff[m[1]] = t.technician_id || t.name } - const res = { ok: true, scope, dryRun, targets: staffIds.length, linked: 0, created: 0, already: 0, no_email: [], samples: [] } + const ext = await extEmailIndex() + const res = { ok: true, scope, dryRun, targets: staffIds.length, linked: 0, created: 0, enriched: 0, already: 0, system: 0, no_email: [], samples: [] } for (const sid of staffIds) { const s = byId.get(String(sid)); if (!s) continue - const full = [s.first_name, s.last_name].filter(Boolean).join(' ').trim(); const email = norm(s.email); const techId = techByStaff[String(sid)] || '' + const full = [s.first_name, s.last_name].filter(Boolean).join(' ').trim(); const techId = techByStaff[String(sid)] || '' + if (SYS_STAFF_RE.test(full)) { res.system++; continue } // compte système → pas d'identité personne + // TOUS les courriels connus (legacy → primaire préféré, puis Employee/Authentik) : le 1er = primaire, le reste = alias. + const legacyMail = norm(s.email) + const allEmails = [...new Set([legacyMail, ...(ext.get(norm(full)) || [])].filter(Boolean))] const existing = idn.resolveIdentity(String(sid)) if (existing && String(existing.legacy_staff_id) === String(sid)) { - if (techId && !existing.tech_id) { if (!dryRun) idn.upsert({ key: existing.key, label: existing.label, primary_email: existing.primary_email, legacy_staff_id: sid, tech_id: techId }); res.linked++; if (res.samples.length < 30) res.samples.push({ staff: sid, action: 'fill-tech', key: existing.key }) } else res.already++ + // Déjà lié : enrichit quand même (tech_id manquant / NOUVEAUX courriels → alias, union jamais overwrite). + const have = new Set([existing.primary_email, ...(existing.alias_emails || [])].map(x => String(x || '').toLowerCase())) + const newMails = allEmails.filter(e => !have.has(e)) + const needTech = techId && !existing.tech_id + if (needTech || newMails.length) { + if (!dryRun) idn.upsert({ key: existing.key, label: existing.label, primary_email: existing.primary_email, legacy_staff_id: sid, tech_id: techId || existing.tech_id, alias_emails: newMails }) + res.enriched++; if (res.samples.length < 40) res.samples.push({ staff: sid, action: 'enrich', key: existing.key, add_aliases: newMails, tech: needTech ? techId : undefined }) + } else res.already++ continue } - const cand = (email && idn.resolveIdentity(email)) || (full && idn.resolveIdentity(full)) || null - if (cand) { if (!dryRun) idn.upsert({ key: cand.key, label: cand.label, primary_email: cand.primary_email, legacy_staff_id: sid, tech_id: techId || cand.tech_id }); res.linked++; if (res.samples.length < 30) res.samples.push({ staff: sid, name: full, action: 'link', key: cand.key, tech: !!techId }); continue } - if (email) { if (!dryRun) idn.upsert({ label: full || email, primary_email: email, legacy_staff_id: sid, tech_id: techId, kind: 'employee', active: Number(s.status) === 1 }); res.created++; if (res.samples.length < 30) res.samples.push({ staff: sid, name: full, action: 'create', email, tech: !!techId }); continue } + // Identité existante par un des courriels ou par nom → LINK (renseigne legacy_staff_id + alias découverts). + let cand = null + for (const e of allEmails) { cand = idn.resolveIdentity(e); if (cand) break } + if (!cand && full) cand = idn.resolveIdentity(full) + if (cand) { if (!dryRun) idn.upsert({ key: cand.key, label: cand.label, primary_email: cand.primary_email, alias_emails: allEmails, legacy_staff_id: sid, tech_id: techId || cand.tech_id }); res.linked++; if (res.samples.length < 40) res.samples.push({ staff: sid, name: full, action: 'link', key: cand.key, tech: !!techId }); continue } + // Aucune identité → CREATE avec le 1er courriel connu en primaire + les autres en alias. + if (allEmails.length) { if (!dryRun) idn.upsert({ label: full || allEmails[0], primary_email: allEmails[0], alias_emails: allEmails.slice(1), legacy_staff_id: sid, tech_id: techId, kind: 'employee', active: Number(s.status) === 1 }); res.created++; if (res.samples.length < 40) res.samples.push({ staff: sid, name: full, action: 'create', primary: allEmails[0], aliases: allEmails.slice(1), tech: !!techId }); continue } res.no_email.push({ staff: sid, name: full }) } return res