fix(mi-dia): leads y oportunidades en pestañas separadas + limpieza

- misLeads: solo muestra items del tablero Leads (captura ligera)
- misOportunidades: muestra items del tablero Oportunidades (afluencia)
- Eliminado el merge que causaba que el mismo item apareciera en ambas
- apartadosPorVencer: filtrado por esMio (solo propios)
- Limpiado duplicado MONICA RODRIGUEZ VEGA de Monday Leads
- 630 tests verdes
This commit is contained in:
2026-09-06 20:22:55 +00:00
parent e466013697
commit aadbc6f83f
4 changed files with 26 additions and 48 deletions

View File

@@ -105,7 +105,7 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
expect(mondayMock.getLeads).not.toHaveBeenCalled() expect(mondayMock.getLeads).not.toHaveBeenCalled()
expect(mondayMock.getOportunidades).not.toHaveBeenCalled() expect(mondayMock.getOportunidades).not.toHaveBeenCalled()
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown> const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
expect((resumen['misLeads'] as unknown[]).length).toBe(2) expect((resumen["misLeads"] as unknown[]).length).toBe(1) // solo leads, no oportunidades
expect((resumen['misOportunidades'] as { items: unknown[] }).items).toHaveLength(1) expect((resumen['misOportunidades'] as { items: unknown[] }).items).toHaveLength(1)
expect(resumen['misActividadesHoy']).toEqual([]) expect(resumen['misActividadesHoy']).toEqual([])
expect(resumen['conteos']).toMatchObject({ actividadesAbiertas: 0 }) expect(resumen['conteos']).toMatchObject({ actividadesAbiertas: 0 })

View File

@@ -62,9 +62,8 @@ describe('resumenMiDiaLocal (Monday caído)', () => {
const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA) const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA)
expect(r.sinMapeoMonday).toBe(false) expect(r.sinMapeoMonday).toBe(false)
expect(r.misLeads.map((l) => l.id).sort()).toEqual(['loc_l1', 'loc_o1']) expect(r.misLeads.map((l) => l.id).sort()).toEqual(['loc_l1']) // solo leads, no oportunidades
expect(r.misLeads.find((l) => l.id === 'loc_l1')?.fuente).toBe('lead') expect(r.misLeads.find((l) => l.id === 'loc_l1')?.fuente).toBe('lead')
expect(r.misLeads.find((l) => l.id === 'loc_o1')?.fuente).toBe('oportunidad')
// porEtapa cuenta TODAS las oportunidades (incluye terminales, como Monday) // porEtapa cuenta TODAS las oportunidades (incluye terminales, como Monday)
expect(r.misOportunidades.porEtapa).toEqual({ Afluencia: 1, Perdido: 1 }) expect(r.misOportunidades.porEtapa).toEqual({ Afluencia: 1, Perdido: 1 })
expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['loc_o1']) expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['loc_o1'])

View File

@@ -126,8 +126,8 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
}) })
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.sinMapeoMonday).toBe(false)
expect(r.misLeads.map((l) => l.id)).toEqual(['l2', 'o1', 'o2', 'l1']) // updatedAt desc mezclando tableros expect(r.misLeads.map((l) => l.id)).toEqual(['l2', 'l1']) // solo leads, no oportunidades // updatedAt desc mezclando tableros
expect(r.misLeads.map((l) => l.fuente)).toEqual(['lead', 'oportunidad', 'oportunidad', 'lead']) expect(r.misLeads.map((l) => l.fuente)).toEqual(['lead', 'lead']) // solo leads
expect(r.misLeads[0]?.telefono).toBe('4771234567') expect(r.misLeads[0]?.telefono).toBe('4771234567')
}) })
@@ -147,12 +147,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
usuario: 'Martin Zavala', usuario: 'Martin Zavala',
detalle: 'Actualización', detalle: 'Actualización',
}) })
// usuarioId sin nombre en el mapa → fallback null // 'o1' ya no esta en misLeads (es oportunidad)
expect(r.misLeads.find((l) => l.id === 'o1')?.ultimoCambio).toEqual({
fechaISO: '2026-08-28T09:00:00.000Z',
usuario: null,
detalle: 'Alta',
})
}) })
it('ultimoCambio: null cuando el item no tiene eventos en el activity log', async () => { it('ultimoCambio: null cuando el item no tiene eventos en el activity log', async () => {
@@ -562,7 +557,7 @@ describe('resumenMiDia — cuenta compartida / uso solo-app (sin Monday)', () =>
{ leadId: 'o1', nombreLead: 'Lead o1', telefono: null, createdAt: new Date('2026-08-27') }, { leadId: 'o1', nombreLead: 'Lead o1', telefono: null, createdAt: new Date('2026-08-27') },
] ]
const r = await resumenMiDia(monday, sinMapeo, new Map(), capturas, [], AHORA) const r = await resumenMiDia(monday, sinMapeo, new Map(), capturas, [], AHORA)
expect(r.misLeads.map((l) => l.id)).toEqual(['o1', 'l1']) expect(r.misLeads.map((l) => l.id)).toEqual(['l1']) // solo leads
expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['o1']) expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['o1'])
}) })

View File

@@ -282,11 +282,9 @@ export async function resumenMiDia(
fuente: 'oportunidad', fuente: 'oportunidad',
ultimoCambio: null, ultimoCambio: null,
})) }))
// Excluir de "Mis leads" los que ya aparecen en "Mis oportunidades" // "Mis leads" solo muestra items del tablero Leads (captura ligera).
// para no mostrar el mismo item en dos pestañas // Las oportunidades van exclusivamente en "Mis oportunidades".
const idsEnOportunidades = new Set(misLeadsOportunidades.map((o) => o.id)) const misLeads: LeadMiDia[] = misLeadsLeads.sort((a, b) =>
const misLeadsLeadsSinDuplicados = misLeadsLeads.filter((l) => !idsEnOportunidades.has(l.id))
const misLeads: LeadMiDia[] = [...misLeadsLeadsSinDuplicados, ...misLeadsOportunidades].sort((a, b) =>
b.updatedAt.localeCompare(a.updatedAt), b.updatedAt.localeCompare(a.updatedAt),
) )
@@ -589,37 +587,23 @@ export function resumenMiDiaLocal(
const oportunidadesLocales = capturas.filter((c) => c.board === 'OPORTUNIDADES') const oportunidadesLocales = capturas.filter((c) => c.board === 'OPORTUNIDADES')
const apartadosLocales = capturas.filter((c) => c.board === 'APARTADO') const apartadosLocales = capturas.filter((c) => c.board === 'APARTADO')
// "Mis leads" mezcla Leads + Oportunidades, igual que el camino Monday // "Mis leads" solo muestra leads (board LEADS), no oportunidades
const misLeads: LeadMiDia[] = [ const misLeads: LeadMiDia[] = leadsLocales
...leadsLocales .filter((c) => !esEstadoTerminalLead(c.etapa))
.filter((c) => !esEstadoTerminalLead(c.etapa)) .map((c) => ({
.map((c) => ({ id: c.leadId,
id: c.leadId, nombre: c.nombreLead,
nombre: c.nombreLead, telefono: c.telefono ?? '',
telefono: c.telefono ?? '', email: c.email ?? '',
email: c.email ?? '', estado: c.etapa,
estado: c.etapa, origen: c.origen ?? '',
origen: c.origen ?? '', updatedAt: c.updatedAt.toISOString(),
updatedAt: c.updatedAt.toISOString(), fuente: 'lead' as const,
fuente: 'lead' as const, ultimoCambio: ultimoCambioLocal(c, usuario.nombre),
ultimoCambio: ultimoCambioLocal(c, usuario.nombre), pendienteSync: c.pendienteSync,
pendienteSync: c.pendienteSync, }))
})), .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
...oportunidadesLocales // (oportunidades se muestran exclusivamente en misOportunidades)
.filter((c) => !esEtapaTerminal(c.etapa))
.map((c) => ({
id: c.leadId,
nombre: c.nombreLead,
telefono: c.telefono ?? '',
email: c.email ?? '',
estado: c.etapa,
origen: c.origen ?? '',
updatedAt: c.updatedAt.toISOString(),
fuente: 'oportunidad' as const,
ultimoCambio: ultimoCambioLocal(c, usuario.nombre),
pendienteSync: c.pendienteSync,
})),
].sort(porUpdatedAtDesc)
// Conteo por etapa de TODAS las oportunidades (incluye terminales, como Monday) // Conteo por etapa de TODAS las oportunidades (incluye terminales, como Monday)
const porEtapa: Record<string, number> = {} const porEtapa: Record<string, number> = {}