fix(afluencia): validación duplicados + campos nombre + fix lada

- Lead creation: validación por email y teléfono normalizados (últimos 10 dígitos)
- Si ya existe lead con mismo email/teléfono → 409 con aviso
- Alta Afluencia: nombre dividido en Nombre(s), Apellido Paterno, Apellido Materno
- Fix lada: strip robusto de +52 y +1 con/sin espacio
- Backend + frontend actualizados
This commit is contained in:
2026-09-08 09:36:46 +00:00
parent 06d4e154a5
commit f648430da0
3 changed files with 73 additions and 12 deletions

View File

@@ -212,6 +212,18 @@ export async function crearLead(
const existente = await monday.buscarLeadPorTelefono(datos.telefono) const existente = await monday.buscarLeadPorTelefono(datos.telefono)
if (existente) return { duplicado: { id: existente.id, nombre: existente.nombre } } if (existente) return { duplicado: { id: existente.id, nombre: existente.nombre } }
} }
// Dedup por email en ambos tableros
if (datos.email !== undefined && datos.email.trim() !== '') {
const [leads, oportunidades] = await Promise.all([
monday.getLeads(),
monday.getOportunidades(),
])
const emailLower = datos.email.trim().toLowerCase()
const existenteLead = leads.find((l) => l.email?.toLowerCase() === emailLower)
if (existenteLead) return { duplicado: { id: existenteLead.id, nombre: existenteLead.nombre } }
const existenteOp = oportunidades.find((o) => o.email?.toLowerCase() === emailLower)
if (existenteOp) return { duplicado: { id: existenteOp.id, nombre: existenteOp.nombre } }
}
if (datos.origen !== undefined) { if (datos.origen !== undefined) {
const labels = await monday.labelsDeColumna(boardLeads, 'Origen') const labels = await monday.labelsDeColumna(boardLeads, 'Origen')

View File

@@ -139,12 +139,23 @@ export class CrmLocalService {
* sincronizarse. * sincronizarse.
*/ */
async crearLead(datos: DatosLeadLocal, usuarioId?: string): Promise<ResultadoCapturaLocal> { async crearLead(datos: DatosLeadLocal, usuarioId?: string): Promise<ResultadoCapturaLocal> {
if (datos.telefono !== undefined) { // Dedup por teléfono (normalizando: últimos 10 dígitos)
if (datos.telefono !== undefined && datos.telefono.trim() !== '') {
const telLimpio = datos.telefono.replace(/\s+/g, '').slice(-10)
if (telLimpio.length >= 7) {
const existente = await prisma.leadCaptura.findFirst({ const existente = await prisma.leadCaptura.findFirst({
where: { telefono: datos.telefono, board: 'LEADS' }, where: { board: 'LEADS', telefono: { contains: telLimpio } },
}) })
if (existente) return { duplicado: { id: existente.leadId, nombre: existente.nombreLead } } if (existente) return { duplicado: { id: existente.leadId, nombre: existente.nombreLead } }
} }
}
// Dedup por email
if (datos.email !== undefined && datos.email.trim() !== '') {
const existenteEmail = await prisma.leadCaptura.findFirst({
where: { email: datos.email.trim(), board: 'LEADS' },
})
if (existenteEmail) return { duplicado: { id: existenteEmail.leadId, nombre: existenteEmail.nombreLead } }
}
const id = idLocal() const id = idLocal()
const datosMonday = { const datosMonday = {
nombre: datos.nombre, nombre: datos.nombre,

View File

@@ -39,6 +39,9 @@ interface Errores {
const FORM_INICIAL = { const FORM_INICIAL = {
nombre: '', nombre: '',
nombrePila: '',
apellidoPaterno: '',
apellidoMaterno: '',
telefono: '', telefono: '',
email: '', email: '',
origen: '', origen: '',
@@ -95,7 +98,12 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
// Teléfono con prefijo internacional: separa lada y número para el selector // Teléfono con prefijo internacional: separa lada y número para el selector
const tel = lead.telefono.trim() const tel = lead.telefono.trim()
const esUsa = tel.startsWith('+1') const esUsa = tel.startsWith('+1')
const numero = tel.startsWith('+1') ? tel.slice(2).trim() : tel.startsWith('+52') ? tel.slice(3).trim() : tel // Strip lada: +52 or +1 prefix
let numero = tel
if (tel.startsWith('+52 ')) numero = tel.slice(4).trim()
else if (tel.startsWith('+52')) numero = tel.slice(3).trim()
else if (tel.startsWith('+1 ')) numero = tel.slice(3).trim()
else if (tel.startsWith('+1')) numero = tel.slice(2).trim()
setLada(esUsa ? 'US' : 'MX') setLada(esUsa ? 'US' : 'MX')
setForm((prev) => { setForm((prev) => {
const desarrollo = const desarrollo =
@@ -107,9 +115,17 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
lead.modeloInteres !== undefined && (modelos as readonly string[]).includes(lead.modeloInteres) lead.modeloInteres !== undefined && (modelos as readonly string[]).includes(lead.modeloInteres)
? lead.modeloInteres ? lead.modeloInteres
: '' : ''
// Split name: lead.nombre may contain full name or just first name
const partes = lead.nombre.trim().split(/\s+/)
const nombrePila = partes[0] || ''
const apellidoPaterno = partes.length > 1 ? partes[1] : ''
const apellidoMaterno = partes.length > 2 ? partes.slice(2).join(' ') : ''
return { return {
...prev, ...prev,
nombre: lead.nombre, nombre: lead.nombre,
nombrePila,
apellidoPaterno,
apellidoMaterno,
telefono: numero, telefono: numero,
email: lead.email, email: lead.email,
origen: (ORIGENES_LEAD as readonly string[]).includes(lead.origen) ? lead.origen : prev.origen, origen: (ORIGENES_LEAD as readonly string[]).includes(lead.origen) ? lead.origen : prev.origen,
@@ -142,7 +158,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
const telefono = const telefono =
numeroTel === '' ? '' : numeroTel.startsWith('+') ? numeroTel : `${lada === 'US' ? '+1' : '+52'} ${numeroTel}` numeroTel === '' ? '' : numeroTel.startsWith('+') ? numeroTel : `${lada === 'US' ? '+1' : '+52'} ${numeroTel}`
const res = await crearAfluencia({ const res = await crearAfluencia({
nombre: form.nombre.trim(), nombre: [form.nombrePila, form.apellidoPaterno, form.apellidoMaterno].filter(Boolean).join(' ').trim() || form.nombre.trim(),
telefono, telefono,
...(oportunidadId !== undefined ? { oportunidadId } : {}), ...(oportunidadId !== undefined ? { oportunidadId } : {}),
...(form.email.trim() ? { email: form.email.trim() } : {}), ...(form.email.trim() ? { email: form.email.trim() } : {}),
@@ -165,7 +181,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
if (res.ok) { if (res.ok) {
setLeadCreado({ setLeadCreado({
id: res.lead.id, id: res.lead.id,
nombre: form.nombre.trim(), nombre: [form.nombrePila, form.apellidoPaterno, form.apellidoMaterno].filter(Boolean).join(' ').trim() || form.nombre.trim(),
telefono, telefono,
email: form.email.trim(), email: form.email.trim(),
estado: res.lead.estado, estado: res.lead.estado,
@@ -262,20 +278,42 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
</div> </div>
)} )}
<div className="grid gap-6 sm:grid-cols-2"> <div className="grid gap-6 sm:grid-cols-2">
<label htmlFor="afluencia-nombre" className="block sm:col-span-2"> <label htmlFor="afluencia-nombre-pila" className="block">
<span className={labelClase}> <span className={labelClase}>
Nombre<span className="text-red-700"> *</span> Nombre(s)<span className="text-red-700"> *</span>
</span> </span>
<input <input
id="afluencia-nombre" id="afluencia-nombre-pila"
type="text" type="text"
value={form.nombre} value={form.nombrePila}
onChange={cambiar('nombre')} onChange={cambiar('nombrePila')}
className={`${inputClase} ${errores.nombre ? 'border-red-400' : ''}`} className={`${inputClase} ${errores.nombre ? 'border-red-400' : ''}`}
/> />
{errores.nombre && <p className="mt-1 text-xs text-red-700">{errores.nombre}</p>} {errores.nombre && <p className="mt-1 text-xs text-red-700">{errores.nombre}</p>}
</label> </label>
<label htmlFor="afluencia-apellido-paterno" className="block">
<span className={labelClase}>Apellido paterno</span>
<input
id="afluencia-apellido-paterno"
type="text"
value={form.apellidoPaterno}
onChange={cambiar('apellidoPaterno')}
className={inputClase}
/>
</label>
<label htmlFor="afluencia-apellido-materno" className="block">
<span className={labelClase}>Apellido materno</span>
<input
id="afluencia-apellido-materno"
type="text"
value={form.apellidoMaterno}
onChange={cambiar('apellidoMaterno')}
className={inputClase}
/>
</label>
<div> <div>
<label htmlFor="afluencia-telefono" className={labelClase}> <label htmlFor="afluencia-telefono" className={labelClase}>
Teléfono<span className="text-red-700"> *</span> Teléfono<span className="text-red-700"> *</span>