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:
@@ -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')
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user