feat(leads): doble via de captura — lead rapido a Leads, afluencia a Oportunidades

- POST /api/leads: captura ligera al tablero Leads (sin oportunidad),
  self-assign a Vendedor o round-robin; tambien cubre la captura manual
  desde la UI de Monday (webhook create_item re-registrado).
- POST /api/afluencias (nuevo): la forma completa sigue cayendo en
  Oportunidades con todo el perfil (etapa Afluencia, columna Asesor).
- Round-robin dual (ambos tableros), Mi dia mezcla leads+oportunidades
  con insignia de origen, acciones con parametro fuente, reportes:
  leads desde Leads, perfilamiento desde Oportunidades, funnel
  Leads->Afluencia->...->Ganado.
This commit is contained in:
2026-09-04 06:09:38 +00:00
parent 772fdef09f
commit 135e3e6de5
21 changed files with 1053 additions and 326 deletions

View File

@@ -56,7 +56,7 @@ describe('AltaAfluenciaForm', () => {
'Registrado en afluencia — se asignará por round-robin',
)
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toContain('/leads')
expect(url).toContain('/afluencias')
expect(JSON.parse(String(init.body))).toMatchObject({
nombre: 'Ana Demo',
telefono: '6641234567',

View File

@@ -1,6 +1,6 @@
import { useState, type FormEvent } from 'react'
import { z } from 'zod'
import { crearLead, type LeadResumen } from '../services/api'
import { crearAfluencia, type LeadResumen } from '../services/api'
import {
DESARROLLOS_LEAD,
ESTADOS_CIVILES,
@@ -94,7 +94,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
setAviso('')
if (!validar()) return
setEnviando(true)
const res = await crearLead({
const res = await crearAfluencia({
nombre: form.nombre.trim(),
telefono: form.telefono.trim(),
...(form.email.trim() ? { email: form.email.trim() } : {}),

View File

@@ -253,12 +253,12 @@ export type ResultadoCrearLead =
| { ok: false; motivo: 'conexion' }
/**
* POST /leads — captura ligera de afluencia: crea UN item en el tablero
* Oportunidades (etapa Afluencia, con todo el perfil; la asigna el capturista
* o el round-robin). Si el teléfono ya existe el API responde 409 con la
* oportunidad existente (motivo 'duplicado').
* POST /afluencias — Alta Afluencia (forma completa): crea UN item en el
* tablero Oportunidades (etapa Afluencia, con todo el perfil; la asigna el
* capturista o el round-robin). Si el teléfono ya existe el API responde 409
* con la oportunidad existente (motivo 'duplicado').
*/
export async function crearLead(payload: {
export async function crearAfluencia(payload: {
nombre: string
telefono: string
email?: string
@@ -280,7 +280,7 @@ export async function crearLead(payload: {
let res: Response
try {
res = await fetch(
`${API_URL}/leads`,
`${API_URL}/afluencias`,
conAuth({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -288,7 +288,7 @@ export async function crearLead(payload: {
}),
)
} catch (err) {
console.warn('[api] crearLead: sin conexión al servidor', err)
console.warn('[api] crearAfluencia: sin conexión al servidor', err)
return { ok: false, motivo: 'conexion' }
}
manejar401(res)
@@ -299,7 +299,7 @@ export async function crearLead(payload: {
const cuerpo = (await res.json()) as { leadId: string; nombre: string }
return { ok: false, motivo: 'duplicado', leadId: cuerpo.leadId, nombre: cuerpo.nombre }
}
console.warn(`[api] crearLead: HTTP ${res.status}`)
console.warn(`[api] crearAfluencia: HTTP ${res.status}`)
return { ok: false, motivo: 'conexion' }
}