feat: rol manager, arreglos de reportes (cancelacion/afluencias), credito unico, cliente enkontrol, vendedor en bitacora y actualizar afluencia existente
This commit is contained in:
@@ -76,6 +76,8 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
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 }))
|
||||
@@ -152,16 +154,49 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
const enviar = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setAviso('')
|
||||
setDuplicado(null)
|
||||
if (!validar()) return
|
||||
setEnviando(true)
|
||||
// Teléfono con lada: "<prefijo> <número>" (si ya trae +, se respeta tal cual)
|
||||
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: "<prefijo> <número>" (si ya trae +, se respeta tal cual) */
|
||||
const telefonoConLada = () => {
|
||||
const numeroTel = form.telefono.trim()
|
||||
const telefono =
|
||||
numeroTel === '' ? '' : numeroTel.startsWith('+') ? numeroTel : `${lada === 'US' ? '+1' : '+52'} ${numeroTel}`
|
||||
const res = await crearAfluencia({
|
||||
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,
|
||||
...(oportunidadId !== undefined ? { oportunidadId } : {}),
|
||||
...(oportunidadIdElegido !== undefined ? { oportunidadId: oportunidadIdElegido } : {}),
|
||||
...(form.email.trim() ? { email: form.email.trim() } : {}),
|
||||
...(form.origen ? { origen: form.origen } : {}),
|
||||
...(form.desarrollo ? { desarrollo: form.desarrollo } : {}),
|
||||
@@ -177,31 +212,41 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
...(form.califica ? { califica: form.califica } : {}),
|
||||
...(form.loQuierePara.trim() ? { loQuierePara: form.loQuierePara.trim() } : {}),
|
||||
...(form.notas.trim() ? { notas: form.notas.trim() } : {}),
|
||||
})
|
||||
setEnviando(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** Resultado del API: éxito, duplicado (ofrece actualizar) o problema de red */
|
||||
const procesarResultado = (
|
||||
res: Awaited<ReturnType<typeof crearAfluencia>>,
|
||||
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,
|
||||
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')
|
||||
} else if (res.motivo === 'duplicado') {
|
||||
setAvisoTipo('error')
|
||||
setAviso(`Ya existe un lead con ese teléfono: ${res.nombre}.`)
|
||||
} else if (res.motivo === 'conexion') {
|
||||
// El backend es local: el registro YA se guardó aunque el fetch se haya
|
||||
// cortado (timeout/red). Aviso amarillo en vez de error rojo.
|
||||
setAvisoTipo('advertencia')
|
||||
setAviso('La afluencia se registró, pero hubo un problema de conexión. Verifica en Mi día.')
|
||||
} else {
|
||||
setAvisoTipo('error')
|
||||
setAviso('No se pudo registrar el lead. Revisa tu conexión e intenta de nuevo.')
|
||||
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 = () => {
|
||||
@@ -598,9 +643,19 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
)}
|
||||
|
||||
{aviso && avisoTipo === 'error' && (
|
||||
<p role="alert" className="mt-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700">
|
||||
{aviso}
|
||||
</p>
|
||||
<div role="alert" className="mt-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700">
|
||||
<p>{aviso}</p>
|
||||
{duplicado && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void actualizarExistente()}
|
||||
disabled={enviando}
|
||||
className="mt-3 rounded-lg bg-accent px-4 py-2 text-xs font-medium text-white shadow-sm transition-colors hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{enviando ? 'Actualizando…' : 'Actualizar este registro con los datos capturados'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex gap-3">
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function DatosGeneralesCampos({ prefix, compacto = false }: Props
|
||||
/>
|
||||
)}
|
||||
{!compacto && (
|
||||
<CampoTexto nombre={p('usuarioEnkontrol')} etiqueta="Usuario Enkontrol" />
|
||||
<CampoTexto nombre={p('usuarioEnkontrol')} etiqueta="Cliente Enkontrol" />
|
||||
)}
|
||||
</SectionCard>
|
||||
)
|
||||
|
||||
@@ -30,13 +30,14 @@ export default function ViviendaCreditoForm({ valores, onAnterior, onSiguiente }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [proyecto])
|
||||
|
||||
const alternarCredito = (valor: TipoCredito) => {
|
||||
/**
|
||||
* El tipo de crédito es de selección ÚNICA (Infonavit, Apoyo Infonavit,
|
||||
* Cofinavit, Segundo crédito, etc.). Se guarda como arreglo de un elemento
|
||||
* para no cambiar el contrato de datos; volver a tocar el elegido lo quita.
|
||||
*/
|
||||
const seleccionarCredito = (valor: TipoCredito) => {
|
||||
const actuales = metodos.getValues('tiposCredito') ?? []
|
||||
metodos.setValue(
|
||||
'tiposCredito',
|
||||
actuales.includes(valor) ? actuales.filter((t) => t !== valor) : [...actuales, valor],
|
||||
{ shouldDirty: true },
|
||||
)
|
||||
metodos.setValue('tiposCredito', actuales.includes(valor) ? [] : [valor], { shouldDirty: true })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -88,10 +89,11 @@ export default function ViviendaCreditoForm({ valores, onAnterior, onSiguiente }
|
||||
>
|
||||
<input
|
||||
id={`tipoCredito-${tc.valor}`}
|
||||
type="checkbox"
|
||||
type="radio"
|
||||
name="tipoCreditoUnico"
|
||||
className="sr-only"
|
||||
checked={tiposSeleccionados.includes(tc.valor)}
|
||||
onChange={() => alternarCredito(tc.valor)}
|
||||
onChange={() => seleccionarCredito(tc.valor)}
|
||||
/>
|
||||
{tc.etiqueta}
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user