feat(alta): separar en Alta Afluencia y Alta de Cliente

- Nueva pantalla de entrada en /client con dos opciones:
  · Alta Afluencia: captura ligera (nombre, teléfono, email, origen,
    desarrollo de interés, notas) → POST /api/leads (lead + oportunidad
    Afluencia + round-robin, todo ya existente en el API)
  · Alta de Cliente: formato completo actual (picker + wizard intactos)
- Manejo de duplicado por teléfono (409 con nombre del lead existente)
- ?cotizacion=<id> sigue saltando directo al wizard
- Tests: 107 verdes (11 nuevos); smoke en vivo OK y limpio
This commit is contained in:
2026-09-01 20:24:26 +00:00
parent 98fedbd8d3
commit 2b87bf66a7
6 changed files with 515 additions and 7 deletions

View File

@@ -39,11 +39,40 @@ afterEach(() => {
vi.unstubAllGlobals()
})
describe('AltaClientePage — pantalla de entrada', () => {
it('muestra las dos opciones de alta', async () => {
render(<AltaClientePage />)
expect(await screen.findByRole('button', { name: /Alta Afluencia/ })).toBeTruthy()
expect(screen.getByRole('button', { name: /Alta de Cliente/ })).toBeTruthy()
})
it('"Alta de Cliente" navega al picker (borradores + búsqueda de lead)', async () => {
render(<AltaClientePage />)
fireEvent.click(await screen.findByRole('button', { name: /Alta de Cliente/ }))
expect(await screen.findByLabelText(/Buscar lead/)).toBeTruthy()
expect(screen.getByText('Borrador previo')).toBeTruthy()
})
it('"Alta Afluencia" muestra el formulario y "Volver" regresa a la entrada', async () => {
render(<AltaClientePage />)
fireEvent.click(await screen.findByRole('button', { name: /Alta Afluencia/ }))
expect(await screen.findByLabelText(/^Nombre/)).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Volver' }))
expect(await screen.findByRole('button', { name: /Alta de Cliente/ })).toBeTruthy()
})
})
describe('AltaClientePage — alta desde lead de Monday', () => {
it('al elegir un lead el wizard arranca con datosGenerales prellenados', async () => {
render(<AltaClientePage />)
// Picker visible con la sección de búsqueda
// Entrada → Alta de Cliente → picker con la sección de búsqueda
fireEvent.click(await screen.findByRole('button', { name: /Alta de Cliente/ }))
const busqueda = await screen.findByLabelText(/Buscar lead/)
fireEvent.change(busqueda, { target: { value: 'maria' } })
fireEvent.click(await screen.findByText('María Fernanda López'))
@@ -55,3 +84,36 @@ describe('AltaClientePage — alta desde lead de Monday', () => {
expect((screen.getByLabelText(/Celular/i) as HTMLInputElement).value).toBe('4771234567')
})
})
describe('AltaClientePage — alta desde cotización (?cotizacion=<id>)', () => {
it('se salta la entrada y va directo al wizard prellenado', async () => {
window.history.pushState({}, '', '/?cotizacion=cot-1')
fetchMock.mockImplementation((input) => {
const url = String(input)
if (url.includes('/cotizaciones/cot-1')) {
return Promise.resolve(
respuestaOk({
clienteNombre: 'Cliente Prueba',
clienteEmail: 'cliente@correo.com',
clienteTelefono: '6645550000',
desarrollo: 'SENDA',
unidadCodigo: 'U-101',
precioFinal: 1500000,
enganche: 150000,
}),
)
}
return Promise.reject(new Error(`fetch inesperado: ${url}`))
})
try {
render(<AltaClientePage />)
// Wizard directo, sin pasar por la entrada
const nombre = (await screen.findByLabelText(/Nombre completo/)) as HTMLInputElement
expect(nombre.value).toBe('CLIENTE PRUEBA')
expect(screen.queryByRole('button', { name: /Alta Afluencia/ })).toBeNull()
} finally {
window.history.pushState({}, '', '/')
}
})
})