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:
@@ -115,20 +115,19 @@ describe('unidadesEnEtapa', () => {
|
||||
})
|
||||
|
||||
describe('datosFunnel', () => {
|
||||
it('ordena Leads → Oportunidades → Aparto o más → Ganado sin subir', () => {
|
||||
it('ordena Afluencia → Agendo Cita → Cotizador → Aparto o más → Ganado (todo desde porEtapa)', () => {
|
||||
const funnel = datosFunnel(resumen())
|
||||
expect(funnel.map((e) => e.etapa)).toEqual(['Leads', 'Oportunidades', 'Aparto o más', 'Ganado'])
|
||||
expect(funnel.map((e) => e.valor)).toEqual([40, 20, 6, 2]) // Aparto(4) + Ganado(2) acumulados
|
||||
const valores = funnel.map((e) => e.valor)
|
||||
for (let i = 1; i < valores.length; i++) expect(valores[i]!).toBeLessThanOrEqual(valores[i - 1]!)
|
||||
expect(funnel.map((e) => e.etapa)).toEqual(['Afluencia', 'Agendo Cita', 'Cotizador', 'Aparto o más', 'Ganado'])
|
||||
expect(funnel.map((e) => e.valor)).toEqual([8, 0, 5, 6, 2]) // Aparto(4) + Ganado(2) acumulados
|
||||
})
|
||||
|
||||
it('tolera etiquetas con otra capitalización', () => {
|
||||
const r = resumen()
|
||||
r.oportunidades.porEtapa = { aparto: 1, ' GANADO ': 3 }
|
||||
r.oportunidades.porEtapa = { afluencia: 3, ' GANADO ': 3, aparto: 1 }
|
||||
const funnel = datosFunnel(r)
|
||||
expect(funnel[2]?.valor).toBe(4)
|
||||
expect(funnel[3]?.valor).toBe(3)
|
||||
expect(funnel[0]?.valor).toBe(3)
|
||||
expect(funnel[3]?.valor).toBe(4)
|
||||
expect(funnel[4]?.valor).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -104,8 +104,10 @@ export function unidadesEnEtapa(resumen: ResumenReportes, etapa: string): number
|
||||
}
|
||||
|
||||
/**
|
||||
* Datos del funnel comercial: Leads → Oportunidades → Aparto o más → Ganado.
|
||||
* "Aparto o más" es acumulado (Aparto + Ganado) para que el funnel no suba.
|
||||
* Datos del funnel comercial, todo desde el tablero Oportunidades (la
|
||||
* afluencia ES la oportunidad en etapa 'Afluencia'): Afluencia → Agendo Cita
|
||||
* → Cotizador → Aparto o más → Ganado. "Aparto o más" es acumulado
|
||||
* (Aparto + Ganado) porque Aparto es subconjunto del camino a Ganado.
|
||||
*/
|
||||
export function datosFunnel(resumen: ResumenReportes): EtapaFunnel[] {
|
||||
const porEtapa = resumen.oportunidades.porEtapa
|
||||
@@ -115,8 +117,9 @@ export function datosFunnel(resumen: ResumenReportes): EtapaFunnel[] {
|
||||
.reduce((acc, [, v]) => acc + v, 0)
|
||||
const apartoOMas = conteo('APARTO') + conteo('GANADO')
|
||||
return [
|
||||
{ etapa: 'Leads', valor: resumen.leads.total },
|
||||
{ etapa: 'Oportunidades', valor: resumen.oportunidades.total },
|
||||
{ etapa: 'Afluencia', valor: conteo('AFLUENCIA') },
|
||||
{ etapa: 'Agendo Cita', valor: conteo('AGENDO CITA') },
|
||||
{ etapa: 'Cotizador', valor: conteo('COTIZADOR') },
|
||||
{ etapa: 'Aparto o más', valor: apartoOMas },
|
||||
{ etapa: 'Ganado', valor: conteo('GANADO') },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user