import { useEffect, useState, type FormEvent } from 'react' import { z } from 'zod' import { crearAfluencia, type LeadResumen } from '../services/api' import BusquedaLead from './BusquedaLead' import { DESARROLLOS_LEAD, ESTADOS_CIVILES, MODELOS_POR_PROYECTO, OPCIONES_CALIFICA, ORIGENES_LEAD, TIPOS_CREDITO, TIPOS_ECONOMIA, TIPOS_EMPLEO, } from '../data/constantes' interface Props { onVolver: () => void /** Conversión directa: tras registrar la afluencia, abrir el alta completa * prellenada con los datos recién capturados (sin rebuscar el lead) */ onPasarAAltaCliente: (lead: LeadResumen) => void /** Prefill desde Mis oportunidades (Mi día): valores iniciales del formulario */ valoresIniciales?: Partial /** Oportunidad a ACTUALIZAR al enviar (sin duplicar el registro) */ oportunidadId?: string } const emailSchema = z.string().trim().email() const inputClase = 'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20' const labelClase = 'mb-1 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]' interface Errores { nombre?: string telefono?: string email?: string } const FORM_INICIAL = { nombre: '', nombrePila: '', apellidoPaterno: '', apellidoMaterno: '', telefono: '', email: '', origen: '', desarrollo: '', modelo: '', numDependientes: '', edad: '', estadoCivil: '', tipoEmpleo: '', ocupacion: '', rangoSalarial: '', credito: '', tipoEconomia: '', califica: '', loQuierePara: '', notas: '', } /** * Captura ligera del interesado al entrar al embudo (POST /leads): crea el * lead y su oportunidad en etapa Afluencia. El alta completa de * cliente sigue siendo el wizard del picker. */ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valoresIniciales, oportunidadId }: Props) { const [form, setForm] = useState({ ...FORM_INICIAL, ...valoresIniciales }) const [lada, setLada] = useState<'MX' | 'US'>( (valoresIniciales?.telefono ?? '').trim().startsWith('+1') ? 'US' : 'MX', ) const [errores, setErrores] = useState({}) const [enviando, setEnviando] = useState(false) const [leadCreado, setLeadCreado] = useState(null) const [asignacion, setAsignacion] = useState<'capturista' | 'round-robin' | null>(null) const [aviso, setAviso] = useState('') const [avisoTipo, setAvisoTipo] = useState<'error' | 'advertencia'>('error') // Registro existente con ese teléfono: permite actualizarlo en vez de fallar const [duplicado, setDuplicado] = useState<{ id: string; nombre: string } | null>(null) const cambiar = (campo: keyof typeof FORM_INICIAL) => (e: { target: { value: string } }) => { setForm((prev) => ({ ...prev, [campo]: e.target.value })) setErrores((prev) => ({ ...prev, [campo]: undefined })) } // Al cambiar de desarrollo se resetea el modelo (las opciones dependen de él) const cambiarDesarrollo = (e: { target: { value: string } }) => { setForm((prev) => ({ ...prev, desarrollo: e.target.value, modelo: '' })) } const modelosDelDesarrollo = MODELOS_POR_PROYECTO[form.desarrollo as keyof typeof MODELOS_POR_PROYECTO] ?? [] /** * Jala los datos de un lead u oportunidad ya capturados: * nombre/contacto siempre; origen, desarrollo y modelo solo si el valor del * lead existe entre las opciones del formulario. */ const prellenarDesdeLead = (lead: LeadResumen) => { // Teléfono con prefijo internacional: separa lada y número para el selector const tel = lead.telefono.trim() const esUsa = tel.startsWith('+1') // 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') setForm((prev) => { const desarrollo = lead.desarrollo !== undefined && (DESARROLLOS_LEAD as readonly string[]).includes(lead.desarrollo) ? lead.desarrollo : prev.desarrollo const modelos = MODELOS_POR_PROYECTO[desarrollo as keyof typeof MODELOS_POR_PROYECTO] ?? [] const modelo = lead.modeloInteres !== undefined && (modelos as readonly string[]).includes(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 { ...prev, nombre: lead.nombre, nombrePila, apellidoPaterno, apellidoMaterno, telefono: numero, email: lead.email, origen: (ORIGENES_LEAD as readonly string[]).includes(lead.origen) ? lead.origen : prev.origen, desarrollo, modelo, } }) setErrores({}) setAviso('') } const validar = (): boolean => { const nuevos: Errores = {} if (!form.nombre.trim()) nuevos.nombre = 'El nombre es obligatorio' if (!form.telefono.trim()) nuevos.telefono = 'El teléfono es obligatorio' if (form.email.trim() && !emailSchema.safeParse(form.email).success) { nuevos.email = 'Correo electrónico inválido' } setErrores(nuevos) return Object.keys(nuevos).length === 0 } const enviar = async (e: FormEvent) => { e.preventDefault() setAviso('') setDuplicado(null) if (!validar()) return setEnviando(true) const res = await crearAfluencia(construirPayload(oportunidadId)) setEnviando(false) procesarResultado(res, oportunidadId) } /** Reenvía la captura como ACTUALIZACIÓN del registro existente (duplicado) */ const actualizarExistente = async () => { if (!duplicado) return setAviso('') setEnviando(true) const res = await crearAfluencia(construirPayload(duplicado.id)) setEnviando(false) if (res.ok) { setDuplicado(null) procesarResultado(res, duplicado.id) return } if (res.motivo === 'duplicado') { setDuplicado({ id: res.leadId, nombre: res.nombre }) setAvisoTipo('error') setAviso(`No se pudo actualizar: el teléfono pertenece a ${res.nombre}.`) return } setAvisoTipo('advertencia') setAviso('La afluencia se actualizó, pero hubo un problema de conexión. Verifica en Mi día.') } /** Teléfono con lada: " " (si ya trae +, se respeta tal cual) */ const telefonoConLada = () => { const numeroTel = form.telefono.trim() return numeroTel === '' ? '' : numeroTel.startsWith('+') ? numeroTel : `${lada === 'US' ? '+1' : '+52'} ${numeroTel}` } /** Payload del alta/actualización de afluencia (teléfono normalizado con lada) */ const construirPayload = (oportunidadIdElegido?: string) => { const telefono = telefonoConLada() return { nombre: [form.nombrePila, form.apellidoPaterno, form.apellidoMaterno].filter(Boolean).join(' ').trim() || form.nombre.trim(), telefono, ...(oportunidadIdElegido !== undefined ? { oportunidadId: oportunidadIdElegido } : {}), ...(form.email.trim() ? { email: form.email.trim() } : {}), ...(form.origen ? { origen: form.origen } : {}), ...(form.desarrollo ? { desarrollo: form.desarrollo } : {}), ...(form.modelo ? { modelo: form.modelo } : {}), ...(form.numDependientes ? { numDependientes: form.numDependientes } : {}), ...(form.edad.trim() ? { edad: form.edad.trim() } : {}), ...(form.estadoCivil ? { estadoCivil: form.estadoCivil } : {}), ...(form.tipoEmpleo ? { tipoEmpleo: form.tipoEmpleo } : {}), ...(form.ocupacion.trim() ? { ocupacion: form.ocupacion.trim() } : {}), ...(form.rangoSalarial.trim() ? { rangoSalarial: form.rangoSalarial.trim() } : {}), ...(form.credito ? { credito: form.credito } : {}), ...(form.tipoEconomia ? { tipoEconomia: form.tipoEconomia } : {}), ...(form.califica ? { califica: form.califica } : {}), ...(form.loQuierePara.trim() ? { loQuierePara: form.loQuierePara.trim() } : {}), ...(form.notas.trim() ? { notas: form.notas.trim() } : {}), } } /** Resultado del API: éxito, duplicado (ofrece actualizar) o problema de red */ const procesarResultado = ( res: Awaited>, oportunidadIdElegido?: string, ) => { if (res.ok) { setLeadCreado({ id: res.lead.id, nombre: [form.nombrePila, form.apellidoPaterno, form.apellidoMaterno].filter(Boolean).join(' ').trim() || form.nombre.trim(), telefono: telefonoConLada(), email: form.email.trim(), estado: res.lead.estado, origen: form.origen, fuente: 'oportunidad', // la captura nueva vive en el tablero Oportunidades }) setAsignacion(res.lead.asignacion ?? 'round-robin') return } if (res.motivo === 'duplicado') { setDuplicado({ id: res.leadId, nombre: res.nombre }) setAvisoTipo('error') setAviso(`Ya existe un registro con ese teléfono: ${res.nombre}.`) return } if (oportunidadIdElegido !== undefined) { // El backend es local: la actualización YA se guardó aunque el fetch se cortara setAvisoTipo('advertencia') setAviso('La afluencia se actualizó, pero hubo un problema de conexión. Verifica en Mi día.') return } setAvisoTipo('advertencia') setAviso('La afluencia se registró, pero hubo un problema de conexión. Verifica en Mi día.') } const capturarOtro = () => { setForm(FORM_INICIAL) setLada('MX') setErrores({}) setAviso('') setLeadCreado(null) setAsignacion(null) } // Redirigir a Mi día 2 s después de registrar la afluencia useEffect(() => { if (leadCreado === null) return const timer = setTimeout(() => { window.location.href = '/hoy/' }, 2000) return () => clearTimeout(timer) }, [leadCreado]) if (leadCreado !== null) { return (

{oportunidadId !== undefined ? 'Afluencia actualizada — el perfil completo quedó guardado en la oportunidad' : asignacion === 'capturista' ? 'Registrado en afluencia — asignado a ti' : 'Registrado en afluencia — se asignará por round-robin'}

Redirigiendo a Mi día en unos segundos…

{oportunidadId === undefined && ( )}
) } return (
void enviar(e)} noValidate> {oportunidadId !== undefined ? (

Estás completando la afluencia de {form.nombre || 'esta oportunidad'} — al enviar se actualiza su oportunidad ya capturada (no se duplica).

) : (

Si el interesado ya está capturado (lead u oportunidad), sus datos se prellenan solos.

)}
{errores.telefono &&

{errores.telefono}

}
{aviso && avisoTipo === 'advertencia' && (

{aviso}

)} {aviso && avisoTipo === 'error' && (

{aviso}

{duplicado && ( )}
)}
) }