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:
@@ -247,4 +247,128 @@ describe('MondayService', () => {
|
||||
expect(parsed['date_fc']).toEqual({ date: '2026-08-21' })
|
||||
expect(parsed['text_t1']).toBeUndefined() // no se envían columnas vacías
|
||||
})
|
||||
|
||||
describe('getLeads / mapearLead (perfilamiento)', () => {
|
||||
const COLUMNAS_LEADS_BASE = [
|
||||
{ id: 'people_v', title: 'Vendedor', type: 'people' },
|
||||
{ id: 'color_e', title: 'Estado', type: 'status' },
|
||||
]
|
||||
const COLUMNAS_PERFIL = [
|
||||
{ id: 'color_des', title: 'Desarrollo', type: 'status' },
|
||||
{ id: 'color_mod', title: 'Modelo de Interés', type: 'status' },
|
||||
{ id: 'text_mc', title: 'Medio de Contacto', type: 'text' },
|
||||
{ id: 'numeric_edad', title: 'Edad', type: 'numbers' },
|
||||
{ id: 'color_ec', title: 'Estado Civil', type: 'status' },
|
||||
{ id: 'color_te', title: 'Tipo de Empleo', type: 'status' },
|
||||
{ id: 'color_cr', title: 'Crédito', type: 'status' },
|
||||
{ id: 'color_teco', title: 'Tipo de Economía', type: 'status' },
|
||||
{ id: 'color_cal', title: 'Califica', type: 'status' },
|
||||
{ id: 'text_rs', title: 'Rango Salarial', type: 'text' },
|
||||
{ id: 'text_lqp', title: 'Lo Quiere Para', type: 'text' },
|
||||
]
|
||||
const itemLead = (id: string, extras: { id: string; text: string }[] = []) => ({
|
||||
id,
|
||||
name: `Lead ${id}`,
|
||||
created_at: '2026-08-20T10:00:00Z',
|
||||
updated_at: '2026-08-21T10:00:00Z',
|
||||
column_values: [
|
||||
{ id: 'people_v', text: '', value: '{"personsAndTeams":[{"id":106736727,"kind":"person"}]}' },
|
||||
{ id: 'color_e', text: 'NUEVO', value: null },
|
||||
...extras.map((e) => ({ ...e, value: null })),
|
||||
],
|
||||
})
|
||||
|
||||
it('mapea las columnas de perfilamiento cuando existen en el board', async () => {
|
||||
mockearFetch((body) => {
|
||||
if (body.query.includes('columns')) {
|
||||
return respuestaOk({ boards: [{ columns: [...COLUMNAS_LEADS_BASE, ...COLUMNAS_PERFIL] }] })
|
||||
}
|
||||
return respuestaOk({
|
||||
boards: [
|
||||
{
|
||||
items_page: {
|
||||
cursor: null,
|
||||
items: [
|
||||
itemLead('1', [
|
||||
{ id: 'color_des', text: 'ATELIER' },
|
||||
{ id: 'color_mod', text: '1R' },
|
||||
{ id: 'text_mc', text: 'WhatsApp' },
|
||||
{ id: 'numeric_edad', text: '32' },
|
||||
{ id: 'color_ec', text: 'Soltero' },
|
||||
{ id: 'color_te', text: 'Empleado' },
|
||||
{ id: 'color_cr', text: 'Bancario' },
|
||||
{ id: 'color_teco', text: 'Asalariado' },
|
||||
{ id: 'color_cal', text: 'Si' },
|
||||
{ id: 'text_rs', text: '20-30 mil' },
|
||||
{ id: 'text_lqp', text: 'Habitarla' },
|
||||
]),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
const leads = await crearServicio().getLeads()
|
||||
expect(leads[0]).toMatchObject({
|
||||
desarrollo: 'ATELIER',
|
||||
modeloInteres: '1R',
|
||||
medioContacto: 'WhatsApp',
|
||||
edad: 32,
|
||||
estadoCivil: 'Soltero',
|
||||
tipoEmpleo: 'Empleado',
|
||||
credito: 'Bancario',
|
||||
tipoEconomia: 'Asalariado',
|
||||
califica: 'Si',
|
||||
rangoSalarial: '20-30 mil',
|
||||
loQuierePara: 'Habitarla',
|
||||
})
|
||||
})
|
||||
|
||||
it('sin las columnas de perfilamiento devuelve \'\' / null y no truena', async () => {
|
||||
mockearFetch((body) => {
|
||||
if (body.query.includes('columns')) return respuestaOk({ boards: [{ columns: COLUMNAS_LEADS_BASE }] })
|
||||
return respuestaOk({ boards: [{ items_page: { cursor: null, items: [itemLead('1')] } }] })
|
||||
})
|
||||
|
||||
const leads = await crearServicio().getLeads()
|
||||
expect(leads[0]).toMatchObject({
|
||||
desarrollo: '',
|
||||
modeloInteres: '',
|
||||
medioContacto: '',
|
||||
edad: null,
|
||||
estadoCivil: '',
|
||||
tipoEmpleo: '',
|
||||
credito: '',
|
||||
tipoEconomia: '',
|
||||
califica: '',
|
||||
rangoSalarial: '',
|
||||
loQuierePara: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('edad no numérica o vacía mapea a null', async () => {
|
||||
mockearFetch((body) => {
|
||||
if (body.query.includes('columns')) {
|
||||
return respuestaOk({ boards: [{ columns: [...COLUMNAS_LEADS_BASE, ...COLUMNAS_PERFIL] }] })
|
||||
}
|
||||
return respuestaOk({
|
||||
boards: [
|
||||
{
|
||||
items_page: {
|
||||
cursor: null,
|
||||
items: [
|
||||
itemLead('1', [{ id: 'numeric_edad', text: 'N/A' }]),
|
||||
itemLead('2', [{ id: 'numeric_edad', text: '' }]),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
const leads = await crearServicio().getLeads()
|
||||
expect(leads.map((l) => l.edad)).toEqual([null, null])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user