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

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