refactor: las afluencias viven en Oportunidades (no en Leads)
- POST /api/leads crea UN solo item en Oportunidades (Estado Afluencia) con todo el perfil (desarrollo, modelo, estado civil, credito, etc.) en las columnas del board; dedup por telefono contra Oportunidades; asignacion al capturista via columna Asesor o round-robin del equipo. - Round-robin, escalado e inactividad operan sobre Oportunidades (solo etapa Afluencia sin asesor entra al reparto). - Acciones del vendedor (contactado/cita/descartar) escriben directo en la oportunidad; descarte = Perdido + Motivo No Compra. - Mi dia y reportes leen Oportunidades; el funnel de /dash arranca en Afluencia; misOportunidades.porEtapa ya es real (columna Asesor). - El picker del alta busca en Leads (historico) Y Oportunidades, con insignia de origen; moverAAparto tolera ambos ids. - Lectura de status con value null = vacio (quirk del default Monday). - Script de migracion del historial de agosto Leads -> Oportunidades.
This commit is contained in:
@@ -206,7 +206,7 @@ describe('AltaAfluenciaForm', () => {
|
||||
})
|
||||
|
||||
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' }))
|
||||
fetchMock.mockResolvedValueOnce(respuesta({ id: 'op-1', estado: 'Afluencia', oportunidadId: 'op-1' }))
|
||||
const onPasar = vi.fn()
|
||||
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={onPasar} />)
|
||||
llenarRequeridos()
|
||||
@@ -216,12 +216,13 @@ describe('AltaAfluenciaForm', () => {
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Pasar a Alta de Cliente/ }))
|
||||
|
||||
expect(onPasar).toHaveBeenCalledWith({
|
||||
id: 'l-1',
|
||||
id: 'op-1',
|
||||
nombre: 'Ana Demo',
|
||||
telefono: '6641234567',
|
||||
email: 'ana@correo.com',
|
||||
estado: 'NUEVO',
|
||||
estado: 'Afluencia',
|
||||
origen: '',
|
||||
fuente: 'oportunidad', // la captura nueva vive en Oportunidades
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,6 +122,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
|
||||
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') {
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { LeadResumen } from '../services/api'
|
||||
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
||||
|
||||
const LEADS: LeadResumen[] = [
|
||||
{ id: 'lead-1', nombre: 'MARÍA FERNANDA LÓPEZ', telefono: '4771234567', email: 'maria@correo.com', estado: 'NUEVO', origen: 'Web' },
|
||||
{ id: 'lead-2', nombre: 'Juan Pérez', telefono: '', email: 'juan@correo.com', estado: 'CONTACTADO', origen: '' },
|
||||
{ id: 'lead-1', nombre: 'MARÍA FERNANDA LÓPEZ', telefono: '4771234567', email: 'maria@correo.com', estado: 'NUEVO', origen: 'Web', fuente: 'lead' },
|
||||
{ id: 'op-2', nombre: 'Juan Pérez', telefono: '', email: 'juan@correo.com', estado: 'Afluencia', origen: '', fuente: 'oportunidad' },
|
||||
]
|
||||
|
||||
const respuestaOk = (data: unknown) =>
|
||||
@@ -35,13 +35,16 @@ describe('BusquedaLead', () => {
|
||||
expect(llamadasBusqueda()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renderiza los resultados de la búsqueda (nombre, teléfono, estado)', async () => {
|
||||
it('renderiza los resultados de la búsqueda (nombre, teléfono, estado) con insignia de fuente', async () => {
|
||||
render(<BusquedaLead onSeleccion={() => undefined} />)
|
||||
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'maria' } })
|
||||
|
||||
expect(await screen.findByText('MARÍA FERNANDA LÓPEZ')).toBeTruthy()
|
||||
expect(screen.getByText(/4771234567 · NUEVO/)).toBeTruthy()
|
||||
expect(screen.getByText('Juan Pérez')).toBeTruthy()
|
||||
// Insignias: gris "Lead" (histórico) y azul "Oportunidad" (modelo nuevo)
|
||||
expect(screen.getByText('Lead')).toBeTruthy()
|
||||
expect(screen.getByText('Oportunidad')).toBeTruthy()
|
||||
const [url] = llamadasBusqueda()[0] as [string]
|
||||
expect(url).toContain('/leads/buscar?q=maria')
|
||||
})
|
||||
|
||||
@@ -8,8 +8,9 @@ interface Props {
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscador con debounce de leads de Monday (GET /leads/buscar). No llama al
|
||||
* API con menos de 2 caracteres; al elegir un resultado avisa con onSeleccion.
|
||||
* Buscador con debounce de leads/afluencias de Monday (GET /leads/buscar,
|
||||
* búsqueda dual: tablero Leads histórico + Oportunidades). No llama al API
|
||||
* con menos de 2 caracteres; al elegir un resultado avisa con onSeleccion.
|
||||
*/
|
||||
export default function BusquedaLead({ onSeleccion }: Props) {
|
||||
const [q, setQ] = useState('')
|
||||
@@ -71,13 +72,25 @@ export default function BusquedaLead({ onSeleccion }: Props) {
|
||||
{resultados !== null && resultados.length > 0 && (
|
||||
<ul className="mt-3 max-w-md divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||
{resultados.map((lead) => (
|
||||
<li key={lead.id}>
|
||||
<li key={`${lead.fuente}-${lead.id}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSeleccion(lead)}
|
||||
className="flex w-full flex-wrap items-baseline justify-between gap-2 px-4 py-3 text-left transition-colors duration-100 hover:bg-[#F8FAFC]"
|
||||
>
|
||||
<span className="text-sm text-[#111827]">{lead.nombre}</span>
|
||||
<span className="flex items-baseline gap-2 text-sm text-[#111827]">
|
||||
{lead.nombre}
|
||||
{/* Insignia de procedencia: gris = archivo histórico, azul = modelo nuevo */}
|
||||
<span
|
||||
className={
|
||||
lead.fuente === 'oportunidad'
|
||||
? 'rounded-full bg-blue-100 px-2 py-0.5 text-[10px] font-medium text-blue-800'
|
||||
: 'rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium text-gray-600'
|
||||
}
|
||||
>
|
||||
{lead.fuente === 'oportunidad' ? 'Oportunidad' : 'Lead'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-xs text-[#64748B]">
|
||||
{lead.telefono || 'sin teléfono'}
|
||||
{lead.estado ? ` · ${lead.estado}` : ''}
|
||||
|
||||
Reference in New Issue
Block a user