fix(busqueda): telefono por digitos normalizados (espacios/ladas/invisibles) y nombres sin acento
This commit is contained in:
@@ -435,18 +435,72 @@ export class CrmLocalService {
|
||||
): Promise<ResultadoBusquedaLocal[]> {
|
||||
const propioLead = alcance?.soloPropio ? { usuarioId: alcance.usuarioId } : {}
|
||||
const propioCliente = alcance?.soloPropio ? { capturadoPorId: alcance.usuarioId } : {}
|
||||
// Búsqueda telefónica: el teléfono crudo puede traer espacios, ladas o
|
||||
// caracteres invisibles (p. ej. '+52 +52 664 782 5023'), así que el match
|
||||
// va contra los dígitos normalizados (telefonoNorm), en ambos sentidos:
|
||||
// quien busca '6647825023' o '+52 664 782 5023' debe encontrar el registro.
|
||||
const digitos = termino.replace(/\D/g, '')
|
||||
const pareceTelefono = /^[\d\s+().-]+$/.test(termino) && digitos.length >= 2
|
||||
// Con lada (+52 ...) el término supera los 10 dígitos del número nacional:
|
||||
// se agrega también el sufijo de 10 dígitos para el match en telefonoNorm
|
||||
const sufijoNacional = digitos.length > 10 ? digitos.slice(-10) : null
|
||||
const condTelefonoLead =
|
||||
digitos.length >= 2
|
||||
? [
|
||||
{ telefono: { contains: termino } },
|
||||
{ telefonoNorm: { contains: digitos } },
|
||||
...(sufijoNacional !== null ? [{ telefonoNorm: { contains: sufijoNacional } }] : []),
|
||||
]
|
||||
: [{ telefono: { contains: termino } }]
|
||||
const condTelefonoCliente = digitos.length >= 2 ? [{ celular: { contains: digitos } }] : []
|
||||
// Nombres: variantes con/sin acento (y ñ/n) porque el asesor no siempre
|
||||
// teclea las tildes ('jazmin' debe encontrar a 'Jazmín'). Se genera la
|
||||
// forma sin acentos más una variante por vocal acentuada (una a la vez),
|
||||
// suficiente en la práctica y acotado en tamaño.
|
||||
const A_QUITA: Record<string, string> = {
|
||||
á: 'a', à: 'a', ä: 'a', â: 'a', é: 'e', è: 'e', ë: 'e', ê: 'e',
|
||||
í: 'i', ï: 'i', î: 'i', ó: 'o', ö: 'o', ô: 'o', ú: 'u', ü: 'u', û: 'u',
|
||||
Á: 'A', À: 'A', Ä: 'A', Â: 'A', É: 'E', È: 'E', Ë: 'E', Ê: 'E',
|
||||
Í: 'I', Ï: 'I', Î: 'I', Ó: 'O', Ö: 'O', Ô: 'O', Ú: 'U', Ü: 'U', Û: 'U',
|
||||
}
|
||||
const A_PONE: Record<string, string> = {
|
||||
a: 'á', e: 'é', i: 'í', o: 'ó', u: 'ú', A: 'Á', E: 'É', I: 'Í', O: 'Ó', U: 'Ú', n: 'ñ', N: 'Ñ',
|
||||
}
|
||||
const variantesNombre = (t: string): string[] => {
|
||||
const base = [...t].map((c) => A_QUITA[c] ?? c).join('')
|
||||
const variants = new Set<string>([base])
|
||||
const chars = [...base]
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const ch = chars[i] as string
|
||||
const acentuada = A_PONE[ch]
|
||||
if (acentuada !== undefined) variants.add(chars.slice(0, i).join('') + acentuada + chars.slice(i + 1).join(''))
|
||||
}
|
||||
if (base !== t) variants.add(t)
|
||||
return [...variants]
|
||||
}
|
||||
const condNombreLead = variantesNombre(termino).map((v) => ({ nombreLead: { contains: v } }))
|
||||
const condNombreCliente = variantesNombre(termino).map((v) => ({ nombreCompleto: { contains: v } }))
|
||||
const capturas = await prisma.leadCaptura.findMany({
|
||||
where: { AND: [propioLead, { OR: [{ nombreLead: { contains: termino } }, { telefono: { contains: termino } }] }] },
|
||||
where: { AND: [propioLead, { OR: [...condNombreLead, ...condTelefonoLead] }] },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 15,
|
||||
take: 50,
|
||||
})
|
||||
const clientes = await prisma.cliente.findMany({
|
||||
where: { AND: [propioCliente, { OR: [{ nombreCompleto: { contains: termino } }, { celular: { contains: termino } }] }] },
|
||||
where: { AND: [propioCliente, { OR: [...condNombreCliente, { celular: { contains: termino } }, ...condTelefonoCliente] }] },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 15,
|
||||
take: 50,
|
||||
})
|
||||
/** Teléfono buscado: match si los dígitos del registro contienen los del
|
||||
* término, o viceversa (búsqueda con lada completa vs registro sin lada). */
|
||||
const coincideTelefono = (crudo: string | null | undefined): boolean => {
|
||||
if (digitos.length < 2) return true
|
||||
const d = (crudo ?? '').replace(/\D/g, '')
|
||||
return d.includes(digitos) || (d.length >= 6 && digitos.includes(d))
|
||||
}
|
||||
const candidatos = [
|
||||
...capturas.map((c) => ({
|
||||
...capturas
|
||||
.filter((c) => !pareceTelefono || coincideTelefono(c.telefono) || c.nombreLead.includes(termino))
|
||||
.map((c) => ({
|
||||
id: c.leadId,
|
||||
nombre: c.nombreLead,
|
||||
telefono: c.telefono ?? '',
|
||||
@@ -458,7 +512,9 @@ export class CrmLocalService {
|
||||
modeloInteres: c.modelo ?? '',
|
||||
orden: c.updatedAt,
|
||||
})),
|
||||
...clientes.map((c) => ({
|
||||
...clientes
|
||||
.filter((c) => !pareceTelefono || coincideTelefono(c.celular) || c.nombreCompleto.includes(termino))
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
nombre: c.nombreCompleto,
|
||||
telefono: c.celular ?? '',
|
||||
|
||||
Reference in New Issue
Block a user