feat(alta): conversión directa afluencia → alta de cliente
Tras registrar la afluencia, la pantalla de éxito ofrece 'Pasar a Alta de Cliente': abre el wizard prellenado con los datos recién capturados (nombre, email, celular) y el leadMondayId ya ligado, sin rebuscar el lead. Quedan también 'Capturar otro' y 'Volver'.
This commit is contained in:
@@ -26,7 +26,7 @@ const llenarRequeridos = () => {
|
|||||||
|
|
||||||
describe('AltaAfluenciaForm', () => {
|
describe('AltaAfluenciaForm', () => {
|
||||||
it('sin nombre ni teléfono no llama al API y muestra los errores', () => {
|
it('sin nombre ni teléfono no llama al API y muestra los errores', () => {
|
||||||
render(<AltaAfluenciaForm onVolver={() => undefined} />)
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||||
|
|
||||||
expect(fetchMock).not.toHaveBeenCalled()
|
expect(fetchMock).not.toHaveBeenCalled()
|
||||||
@@ -35,7 +35,7 @@ describe('AltaAfluenciaForm', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('con email inválido no llama al API', () => {
|
it('con email inválido no llama al API', () => {
|
||||||
render(<AltaAfluenciaForm onVolver={() => undefined} />)
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||||
llenarRequeridos()
|
llenarRequeridos()
|
||||||
fireEvent.change(screen.getByLabelText(/^Email/), { target: { value: 'correo-malo' } })
|
fireEvent.change(screen.getByLabelText(/^Email/), { target: { value: 'correo-malo' } })
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||||
@@ -46,7 +46,7 @@ describe('AltaAfluenciaForm', () => {
|
|||||||
|
|
||||||
it('al registrar (201) muestra el mensaje de éxito', async () => {
|
it('al registrar (201) muestra el mensaje de éxito', async () => {
|
||||||
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
|
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
|
||||||
render(<AltaAfluenciaForm onVolver={() => undefined} />)
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||||
llenarRequeridos()
|
llenarRequeridos()
|
||||||
fireEvent.change(screen.getByLabelText(/^Origen/), { target: { value: 'Web' } })
|
fireEvent.change(screen.getByLabelText(/^Origen/), { target: { value: 'Web' } })
|
||||||
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
|
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
|
||||||
@@ -69,7 +69,7 @@ describe('AltaAfluenciaForm', () => {
|
|||||||
fetchMock.mockResolvedValueOnce(
|
fetchMock.mockResolvedValueOnce(
|
||||||
respuesta({ error: 'Ya existe un lead con ese teléfono', leadId: 'l-9', nombre: 'ANA DEMO' }, 409),
|
respuesta({ error: 'Ya existe un lead con ese teléfono', leadId: 'l-9', nombre: 'ANA DEMO' }, 409),
|
||||||
)
|
)
|
||||||
render(<AltaAfluenciaForm onVolver={() => undefined} />)
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||||
llenarRequeridos()
|
llenarRequeridos()
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ describe('AltaAfluenciaForm', () => {
|
|||||||
|
|
||||||
it('con error de red muestra un mensaje reintentable', async () => {
|
it('con error de red muestra un mensaje reintentable', async () => {
|
||||||
fetchMock.mockRejectedValueOnce(new Error('red caída'))
|
fetchMock.mockRejectedValueOnce(new Error('red caída'))
|
||||||
render(<AltaAfluenciaForm onVolver={() => undefined} />)
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||||
llenarRequeridos()
|
llenarRequeridos()
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ describe('AltaAfluenciaForm', () => {
|
|||||||
|
|
||||||
it('"Capturar otro" limpia el formulario tras el éxito', async () => {
|
it('"Capturar otro" limpia el formulario tras el éxito', async () => {
|
||||||
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
|
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
|
||||||
render(<AltaAfluenciaForm onVolver={() => undefined} />)
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
|
||||||
llenarRequeridos()
|
llenarRequeridos()
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||||
|
|
||||||
@@ -104,8 +104,28 @@ describe('AltaAfluenciaForm', () => {
|
|||||||
|
|
||||||
it('"Volver" avisa a la pantalla de entrada', () => {
|
it('"Volver" avisa a la pantalla de entrada', () => {
|
||||||
const onVolver = vi.fn()
|
const onVolver = vi.fn()
|
||||||
render(<AltaAfluenciaForm onVolver={onVolver} />)
|
render(<AltaAfluenciaForm onVolver={onVolver} onPasarAAltaCliente={() => undefined} />)
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Volver' }))
|
fireEvent.click(screen.getByRole('button', { name: 'Volver' }))
|
||||||
expect(onVolver).toHaveBeenCalled()
|
expect(onVolver).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('tras el éxito, "Pasar a Alta de Cliente" entrega el lead recién creado', async () => {
|
||||||
|
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
|
||||||
|
const onPasar = vi.fn()
|
||||||
|
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={onPasar} />)
|
||||||
|
llenarRequeridos()
|
||||||
|
fireEvent.change(screen.getByLabelText(/^Email/), { target: { value: 'ana@correo.com' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: /Pasar a Alta de Cliente/ }))
|
||||||
|
|
||||||
|
expect(onPasar).toHaveBeenCalledWith({
|
||||||
|
id: 'l-1',
|
||||||
|
nombre: 'Ana Demo',
|
||||||
|
telefono: '6641234567',
|
||||||
|
email: 'ana@correo.com',
|
||||||
|
estado: 'NUEVO',
|
||||||
|
origen: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { crearLead } from '../services/api'
|
import { crearLead, type LeadResumen } from '../services/api'
|
||||||
import { DESARROLLOS_LEAD, ORIGENES_LEAD } from '../data/constantes'
|
import { DESARROLLOS_LEAD, ORIGENES_LEAD } from '../data/constantes'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onVolver: () => void
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailSchema = z.string().trim().email()
|
const emailSchema = z.string().trim().email()
|
||||||
@@ -27,11 +30,11 @@ const FORM_INICIAL = { nombre: '', telefono: '', email: '', origen: '', desarrol
|
|||||||
* lead en Monday y su oportunidad en etapa Afluencia. El alta completa de
|
* lead en Monday y su oportunidad en etapa Afluencia. El alta completa de
|
||||||
* cliente sigue siendo el wizard del picker.
|
* cliente sigue siendo el wizard del picker.
|
||||||
*/
|
*/
|
||||||
export default function AltaAfluenciaForm({ onVolver }: Props) {
|
export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Props) {
|
||||||
const [form, setForm] = useState(FORM_INICIAL)
|
const [form, setForm] = useState(FORM_INICIAL)
|
||||||
const [errores, setErrores] = useState<Errores>({})
|
const [errores, setErrores] = useState<Errores>({})
|
||||||
const [enviando, setEnviando] = useState(false)
|
const [enviando, setEnviando] = useState(false)
|
||||||
const [exito, setExito] = useState(false)
|
const [leadCreado, setLeadCreado] = useState<LeadResumen | null>(null)
|
||||||
const [aviso, setAviso] = useState('')
|
const [aviso, setAviso] = useState('')
|
||||||
|
|
||||||
const cambiar = (campo: keyof typeof FORM_INICIAL) => (e: { target: { value: string } }) => {
|
const cambiar = (campo: keyof typeof FORM_INICIAL) => (e: { target: { value: string } }) => {
|
||||||
@@ -65,7 +68,14 @@ export default function AltaAfluenciaForm({ onVolver }: Props) {
|
|||||||
})
|
})
|
||||||
setEnviando(false)
|
setEnviando(false)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setExito(true)
|
setLeadCreado({
|
||||||
|
id: res.lead.id,
|
||||||
|
nombre: form.nombre.trim(),
|
||||||
|
telefono: form.telefono.trim(),
|
||||||
|
email: form.email.trim(),
|
||||||
|
estado: res.lead.estado,
|
||||||
|
origen: form.origen,
|
||||||
|
})
|
||||||
} else if (res.motivo === 'duplicado') {
|
} else if (res.motivo === 'duplicado') {
|
||||||
setAviso(`Ya existe un lead con ese teléfono: ${res.nombre}.`)
|
setAviso(`Ya existe un lead con ese teléfono: ${res.nombre}.`)
|
||||||
} else {
|
} else {
|
||||||
@@ -77,10 +87,10 @@ export default function AltaAfluenciaForm({ onVolver }: Props) {
|
|||||||
setForm(FORM_INICIAL)
|
setForm(FORM_INICIAL)
|
||||||
setErrores({})
|
setErrores({})
|
||||||
setAviso('')
|
setAviso('')
|
||||||
setExito(false)
|
setLeadCreado(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exito) {
|
if (leadCreado !== null) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<p
|
<p
|
||||||
@@ -89,11 +99,18 @@ export default function AltaAfluenciaForm({ onVolver }: Props) {
|
|||||||
>
|
>
|
||||||
Registrado en afluencia — se asignará por round-robin
|
Registrado en afluencia — se asignará por round-robin
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6 flex gap-3">
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onPasarAAltaCliente(leadCreado)}
|
||||||
|
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
Pasar a Alta de Cliente
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={capturarOtro}
|
onClick={capturarOtro}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900"
|
className="border border-accent bg-white px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
||||||
>
|
>
|
||||||
Capturar otro
|
Capturar otro
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -245,7 +245,10 @@ export default function AltaClientePage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">
|
<main className="mx-auto max-w-6xl px-4 py-16">
|
||||||
<AltaAfluenciaForm onVolver={() => setPantalla('entrada')} />
|
<AltaAfluenciaForm
|
||||||
|
onVolver={() => setPantalla('entrada')}
|
||||||
|
onPasarAAltaCliente={empezarDesdeLead}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user