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

@@ -8,6 +8,8 @@ export interface LeadMiDia {
estado: string
origen: string
updatedAt: string
/** Tablero del item: 'lead' (tablero Leads) u 'oportunidad' (Oportunidades) */
fuente: 'lead' | 'oportunidad'
}
export interface ActividadMiDia {

View File

@@ -133,7 +133,19 @@ function FilaLead({
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-sm font-medium">{lead.nombre}</p>
<p className="flex items-center gap-2 text-sm font-medium">
{lead.nombre}
{/* Insignia de procedencia: gris = tablero Leads, azul = Oportunidades */}
<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>
</p>
<p className="mt-1 text-xs text-[#64748B]">
{lead.telefono || 'Sin teléfono'}
{lead.origen ? ` · ${lead.origen}` : ''}
@@ -366,10 +378,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<ul className="space-y-2">
{resumen.misLeads.map((lead) => (
<FilaLead
key={lead.id}
key={`${lead.fuente}-${lead.id}`}
lead={lead}
enviando={enviando}
onAccion={(estado, fecha) => void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha), 'No se pudo actualizar el lead')}
onAccion={(estado, fecha) => void ejecutar(() => cambiarEstadoLead(lead.id, estado, fecha, lead.fuente), 'No se pudo actualizar el lead')}
onDescartar={() => setDescartando(lead)}
/>
))}
@@ -656,7 +668,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
const lead = descartando
setDescartando(null)
void ejecutar(
() => descartarLead(lead.id, motivo, comentario === '' ? undefined : comentario),
() => descartarLead(lead.id, motivo, comentario === '' ? undefined : comentario, lead.fuente),
'No se pudo descartar el lead',
)
}}

View File

@@ -41,18 +41,26 @@ export async function crearLead(datos: {
})
}
export function cambiarEstadoLead(itemId: string, estado: string, fecha?: string): Promise<unknown> {
export function cambiarEstadoLead(itemId: string, estado: string, fecha?: string, fuente?: 'lead' | 'oportunidad'): Promise<unknown> {
return fetchJson(`/leads/${itemId}/estado`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fecha !== undefined && fecha !== '' ? { estado, fecha } : { estado }),
body: JSON.stringify({
estado,
...(fecha !== undefined && fecha !== '' ? { fecha } : {}),
...(fuente !== undefined ? { fuente } : {}),
}),
})
}
export function descartarLead(itemId: string, motivo: string, comentario?: string): Promise<unknown> {
export function descartarLead(itemId: string, motivo: string, comentario?: string, fuente?: 'lead' | 'oportunidad'): Promise<unknown> {
return fetchJson(`/leads/${itemId}/descartar`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(comentario !== undefined && comentario !== '' ? { motivo, comentario } : { motivo }),
body: JSON.stringify({
motivo,
...(comentario !== undefined && comentario !== '' ? { comentario } : {}),
...(fuente !== undefined ? { fuente } : {}),
}),
})
}