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:
2026-09-04 04:46:34 +00:00
parent d4fc9ee892
commit 845f314560
26 changed files with 1496 additions and 985 deletions

View File

@@ -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)
})
})