feat(7-mejoras): notas persisten + soporte + captura visible

1. Notas propagan entre etapas (convertirLead/apartar updateMany)
2. Afluencia: conexión → aviso amarillo
3. Escrituración: click nombre abre bitácora
4. Co-acreditado: opción 'no tiene'
5. Nota del alta de lead → bitácora
6. Módulo Soporte: formulario + listado solicitudes
7. Captura visible para todos
This commit is contained in:
2026-09-11 23:17:25 +00:00
parent 1ba7aa49da
commit c8357fe582
22 changed files with 693 additions and 19 deletions

View File

@@ -174,14 +174,14 @@ describe('AltaAfluenciaForm', () => {
)
})
it('con error de red muestra un mensaje reintentable', async () => {
it('con error de red muestra aviso amarillo: el registro se guardó igualmente', async () => {
fetchMock.mockRejectedValueOnce(new Error('red caída'))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect((await screen.findByRole('alert')).textContent).toContain(
'No se pudo registrar el lead. Revisa tu conexión e intenta de nuevo.',
expect((await screen.findByRole('status')).textContent).toContain(
'La afluencia se registró, pero hubo un problema de conexión. Verifica en Mi día.',
)
})

View File

@@ -75,6 +75,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
const [leadCreado, setLeadCreado] = useState<LeadResumen | null>(null)
const [asignacion, setAsignacion] = useState<'capturista' | 'round-robin' | null>(null)
const [aviso, setAviso] = useState('')
const [avisoTipo, setAvisoTipo] = useState<'error' | 'advertencia'>('error')
const cambiar = (campo: keyof typeof FORM_INICIAL) => (e: { target: { value: string } }) => {
setForm((prev) => ({ ...prev, [campo]: e.target.value }))
@@ -190,8 +191,15 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
})
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.')
}
}
@@ -583,7 +591,13 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
</label>
</div>
{aviso && (
{aviso && avisoTipo === 'advertencia' && (
<p role="status" className="mt-6 rounded-lg border border-yellow-200 bg-yellow-50 px-4 py-3 text-xs font-medium text-yellow-800">
{aviso}
</p>
)}
{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>

View File

@@ -17,9 +17,28 @@ export default function CoacreditadoForm({ valores, onAnterior, onSiguiente }: P
})
const tieneCoacreditado = metodos.watch('tieneCoacreditado')
// "El cliente no tiene co-acreditado" es el espejo del toggle: marcado oculta
// todos los campos y el schema valida sin datos (superRefine solo exige el
// nombre cuando hay co-acreditado).
const cambiarSinCoacreditado = (e: { target: { checked: boolean } }) => {
metodos.setValue('tieneCoacreditado', !e.target.checked, { shouldValidate: false })
metodos.clearErrors()
}
return (
<FormProvider {...metodos}>
<form onSubmit={metodos.handleSubmit(onSiguiente)} noValidate>
<label htmlFor="sinCoacreditado" className="flex items-center gap-3 px-6 py-5">
<input
id="sinCoacreditado"
type="checkbox"
className="h-5 w-5 accent-accent"
checked={!tieneCoacreditado}
onChange={cambiarSinCoacreditado}
/>
<span className="text-sm font-medium text-[#111827]">El cliente no tiene co-acreditado</span>
</label>
<label htmlFor="tieneCoacreditado" className="flex items-center gap-3 px-6 py-5">
<input
id="tieneCoacreditado"