feat: mis clientes en Mi dia + perfilamiento de la afluencia en dashboards

- Cliente.capturadoPorId: el alta (directa y progresiva) registra al
  usuario que la inicio; el panel Mi dia muestra 'Mis clientes' con su
  estado (En captura / Cliente) sin depender de Monday.
- LeadMonday ahora lee las 11 columnas de perfil del tablero Leads
  (estado civil, edad, rango salarial, tipo de empleo/economia,
  califica, credito, modelo, etc.).
- GET /api/reportes/resumen agrega leads.perfil con conteos por
  dimension (valores vacios se omiten; edad en buckets).
- /dash/ gana la seccion 'Perfilamiento de la afluencia' con
  mini-graficas de barras por dimension.
This commit is contained in:
2026-09-02 02:39:19 +00:00
parent 895a831ff8
commit 4030faee25
18 changed files with 551 additions and 14 deletions

View File

@@ -83,7 +83,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
lead({ id: '6', estado: 'NUEVO', vendedorIds: [OTRO_ID] }),
],
})
const r = await resumenMiDia(monday, vendedor, new Map(), [], AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
expect(r.sinMapeoMonday).toBe(false)
expect(r.misLeads.map((l) => l.id)).toEqual(['1', '2'])
expect(r.misLeads[1]?.telefono).toBe('4771234567')
@@ -101,7 +101,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
actividad({ id: 'a7', fecha: '' }), // sin fecha: no
],
})
const r = await resumenMiDia(monday, vendedor, new Map(), [], AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
// Ordenadas por fecha: vencida primero
expect(r.misActividadesHoy.map((a) => a.id)).toEqual(['a3', 'a1', 'a2'])
expect(r.conteos.actividadesAbiertas).toBe(5) // abiertas mías: a1,a2,a3,a4,a7
@@ -118,7 +118,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
apartado({ id: 'p6', fechaLimite: '2026-09-01', estado: 'Cancelacion' }),
],
})
const r = await resumenMiDia(monday, vendedor, new Map(), [], AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
expect(r.apartadosPorVencer.map((a) => a.id)).toEqual(['p1', 'p2'])
expect(r.apartadosPorVencer[0]?.diasRestantes).toBe(0)
expect(r.apartadosPorVencer[1]?.diasRestantes).toBe(7)
@@ -130,7 +130,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
actividades: [actividad({ id: 'a1' })],
apartados: [apartado({ id: 'p1' })],
})
const r = await resumenMiDia(monday, sinMapeo, new Map(), [], AHORA)
const r = await resumenMiDia(monday, sinMapeo, new Map(), [], [], AHORA)
expect(r.sinMapeoMonday).toBe(true)
expect(r.misLeads).toEqual([])
expect(r.misActividadesHoy).toEqual([])
@@ -148,12 +148,12 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
lead({ id: '4', createdAt: '2026-08-27T08:00:00', vendedorIds: [OTRO_ID] }), // de otro
],
})
const r = await resumenMiDia(monday, vendedor, new Map(), [], AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
expect(r.conteos.leadsNuevosSemana).toBe(2)
})
it('oportunidades se omiten con nota (el board no tiene columna de asesor)', async () => {
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], AHORA)
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], [], AHORA)
expect(r.misOportunidades).toBeNull()
expect(r.notaOportunidades).toContain('asesor')
})
@@ -171,10 +171,10 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
})
const nombres = new Map([[MI_ID, 'Martin Zavala']])
const rVendedor = await resumenMiDia(monday, vendedor, nombres, [], AHORA)
const rVendedor = await resumenMiDia(monday, vendedor, nombres, [], [], AHORA)
expect(rVendedor.equipo).toBeUndefined()
const rCoord = await resumenMiDia(monday, coordinador, nombres, [], AHORA)
const rCoord = await resumenMiDia(monday, coordinador, nombres, [], [], AHORA)
expect(rCoord.equipo?.leadsNuevosSinAsignar).toBe(1)
expect(rCoord.equipo?.porVendedor).toEqual([
{ mondayUserId: OTRO_ID, nombre: null, leadsAsignados: 2, leadsNuevosSemana: 1 },
@@ -185,7 +185,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
it('corporativo y admin también ven equipo', async () => {
const monday = mondayMock({ leads: [lead({ id: '1' })] })
for (const rol of ['corporativo', 'admin']) {
const r = await resumenMiDia(monday, { nombre: 'X', rol, mondayUserId: null }, new Map(), [], AHORA)
const r = await resumenMiDia(monday, { nombre: 'X', rol, mondayUserId: null }, new Map(), [], [], AHORA)
expect(r.equipo).toBeDefined()
}
})
@@ -197,7 +197,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
const capturas = [
{ leadId: '100', nombreLead: 'Ana López', telefono: '6641112233', createdAt: new Date('2026-08-27T09:00:00Z') },
]
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
// Aunque el lead quedó asignado a OTRO por round-robin, la captura es mía
expect(r.misCapturas).toEqual([
{
@@ -216,7 +216,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
const capturas = [
{ leadId: 'borrado-1', nombreLead: 'Lead Borrado', telefono: null, createdAt: new Date('2026-08-26T09:00:00Z') },
]
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
expect(r.misCapturas).toEqual([
{ leadId: 'borrado-1', nombre: 'Lead Borrado', telefono: '', estado: '', origen: '', capturadoEn: '2026-08-26T09:00:00.000Z' },
])
@@ -228,14 +228,65 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
{ leadId: '1', nombreLead: 'Antiguo', telefono: null, createdAt: new Date('2026-08-20T09:00:00Z') },
{ leadId: '2', nombreLead: 'Reciente', telefono: null, createdAt: new Date('2026-08-27T09:00:00Z') },
]
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, AHORA)
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
expect(r.misCapturas.map((c) => c.leadId)).toEqual(['2', '1'])
})
it('sin capturas: misCapturas va vacío', async () => {
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], AHORA)
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], [], AHORA)
expect(r.misCapturas).toEqual([])
})
it('mis clientes: mapeo con completado según proyecto (vacío = en captura)', async () => {
const clientes = [
{
id: 'c1',
nombreCompleto: 'Ana En Captura',
proyecto: '',
unidadCodigo: null,
createdAt: new Date('2026-08-26T09:00:00Z'),
},
{
id: 'c2',
nombreCompleto: 'Luis Completado',
proyecto: 'DIAGONAL',
unidadCodigo: 'U-101',
createdAt: new Date('2026-08-27T09:00:00Z'),
},
]
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], clientes, AHORA)
expect(r.misClientes).toEqual([
{
id: 'c2',
nombre: 'Luis Completado',
proyecto: 'DIAGONAL',
unidadCodigo: 'U-101',
completado: true,
capturadoEn: '2026-08-27T09:00:00.000Z',
},
{
id: 'c1',
nombre: 'Ana En Captura',
proyecto: '',
unidadCodigo: '',
completado: false,
capturadoEn: '2026-08-26T09:00:00.000Z',
},
])
})
it('mis clientes: proyecto con solo espacios cuenta como en captura', async () => {
const clientes = [
{ id: 'c1', nombreCompleto: 'Espacios', proyecto: ' ', unidadCodigo: null, createdAt: new Date('2026-08-26T09:00:00Z') },
]
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], clientes, AHORA)
expect(r.misClientes[0]?.completado).toBe(false)
})
it('sin clientes: misClientes va vacío (no requiere Monday)', async () => {
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], [], AHORA)
expect(r.misClientes).toEqual([])
})
})
describe('clasificadores de estado', () => {