feat: vínculo con usuario Monday en landing + alta prellenada desde leads

API:
- GET /api/monday/usuarios: usuarios activos no-guest del workspace
- PATCH /api/usuarios/me/monday: vincula/desvincula el mondayUserId del
  usuario autenticado (valida contra el workspace)
- GET /api/leads/buscar?q=: búsqueda de leads por nombre/teléfono/email
  (sin acentos, orden updatedAt desc, límite 15)
- aPublico/me ahora incluyen mondayUserId

Landing (/):
- Nueva sección 'Configuración de cuenta': login inline, selector del
  usuario de Monday, guardar/desvincular, cerrar sesión

Alta de clientes (/client):
- Búsqueda de leads de Monday con debounce en la pantalla de entrada;
  al elegir uno prellena nombre/email/celular y guarda leadMondayId
- El picker (borradores + alta nueva + búsqueda de lead) es ahora
  siempre la pantalla de entrada

Tests: api 294, landing 8, alta-clientes 96 — todos verdes
This commit is contained in:
2026-08-28 18:36:08 +00:00
parent d99d90d344
commit 1692dde887
28 changed files with 2501 additions and 41 deletions

View File

@@ -0,0 +1,57 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import AltaClientePage from './AltaClientePage'
import type { LeadResumen } from '../services/api'
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
const LEAD: LeadResumen = {
id: 'lead-9',
nombre: 'María Fernanda López',
telefono: '4771234567',
email: 'maria@correo.com',
estado: 'NUEVO',
origen: 'Web',
}
const respuestaOk = (data: unknown) =>
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
beforeEach(() => {
fetchMock = vi.fn<FetchFn>().mockImplementation((input) => {
const url = String(input)
if (url.includes('/borradores')) {
// Un borrador para que la pantalla inicial sea el picker
return Promise.resolve(
respuestaOk([{ id: 'b-1', nombre: 'Borrador previo', paso: 0, updatedAt: '2026-08-20T10:00:00Z' }]),
)
}
if (url.includes('/leads/buscar')) return Promise.resolve(respuestaOk([LEAD]))
return Promise.reject(new Error(`fetch inesperado: ${url}`))
})
vi.stubGlobal('fetch', fetchMock)
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
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
const busqueda = await screen.findByLabelText(/Buscar lead/)
fireEvent.change(busqueda, { target: { value: 'maria' } })
fireEvent.click(await screen.findByText('María Fernanda López'))
// Wizard paso 1 con los campos del lead ya capturados
const nombre = (await screen.findByLabelText(/Nombre completo/)) as HTMLInputElement
expect(nombre.value).toBe('MARÍA FERNANDA LÓPEZ')
expect((screen.getByLabelText(/Correo electrónico|Email/i) as HTMLInputElement).value).toBe('maria@correo.com')
expect((screen.getByLabelText(/Celular/i) as HTMLInputElement).value).toBe('4771234567')
})
})