import { describe, expect, it } from 'vitest' import { resumenMiDiaLocal, unirPendientesLocales, type ActividadLocalMiDia, type ClienteLocalMiDia, type FilaLocalMiDia, type ResumenMiDia, type UsuarioLocal, type UsuarioMiDia, } from './mi-dia.service.js' /** * Mi día en modo local (Monday caído) y la unión durante la transición * (Monday vivo + capturas locales pendientes de sincronizar). */ const AHORA = new Date('2026-09-07T12:00:00') const vendedor: UsuarioMiDia = { nombre: 'Martin Zavala', rol: 'vendedor', mondayUserId: 123 } const coordinador: UsuarioMiDia = { nombre: 'Coord General', rol: 'coordinador', mondayUserId: 456 } const sinNombre: UsuarioMiDia = { nombre: ' ', rol: 'vendedor', mondayUserId: null } let consecutivo = 0 function fila(parcial: Partial & { leadId: string }): FilaLocalMiDia { consecutivo += 1 return { nombreLead: `Persona ${parcial.leadId}`, telefono: null, email: null, origen: null, desarrollo: null, modelo: null, board: 'LEADS', etapa: 'NUEVO', fechaLimite: null, datosJson: null, usuarioId: 'u1', mondayItemId: null, pendienteSync: true, createdAt: new Date(AHORA.getTime() - consecutivo * 3_600_000), updatedAt: new Date(AHORA.getTime() - consecutivo * 1_800_000), ...parcial, } } function cliente(parcial: Partial & { id: string }): ClienteLocalMiDia { return { nombreCompleto: `Cliente ${parcial.id}`, proyecto: '', unidadCodigo: null, mondayApartadoId: null, createdAt: new Date('2026-09-04T10:00:00'), ...parcial, } } describe('resumenMiDiaLocal (Monday caído)', () => { it('arma el ResumenMiDia completo desde LeadCaptura por board, sin terminales', () => { const capturas = [ fila({ leadId: 'loc_l1', board: 'LEADS', etapa: 'NUEVO' }), fila({ leadId: 'loc_l2', board: 'LEADS', etapa: 'Descartado' }), // terminal: fuera fila({ leadId: 'loc_o1', board: 'OPORTUNIDADES', etapa: 'Afluencia', desarrollo: 'SENDA', modelo: '1R' }), fila({ leadId: 'loc_o2', board: 'OPORTUNIDADES', etapa: 'Perdido' }), // terminal: fuera de listas, cuenta en porEtapa fila({ leadId: 'loc_a1', board: 'APARTADO', etapa: 'Pago Apartado' }), fila({ leadId: 'loc_a2', board: 'APARTADO', etapa: 'Cancelado' }), // final: fuera ] const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA) expect(r.sinMapeoMonday).toBe(false) 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') // porEtapa cuenta TODAS las oportunidades (incluye terminales, como Monday) expect(r.misOportunidades.porEtapa).toEqual({ Afluencia: 1, Perdido: 1 }) expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['loc_o1']) expect(r.misOportunidades.items[0]).toMatchObject({ etapa: 'Afluencia', desarrollo: 'SENDA', modeloInteres: '1R' }) expect(r.misApartados.map((a) => a.id)).toEqual(['loc_a1']) expect(r.misCapturas).toHaveLength(4) // loc_l1 y loc_o1 se excluyen (ya en misLeads/misOportunidades) // Lo que no existe en local va vacío/cero sin romper el front expect(r.misActividadesHoy).toEqual([]) expect(r.apartadosPorVencer).toEqual([]) expect(r.conteos.actividadesAbiertas).toBe(0) expect(r.equipo).toBeUndefined() }) it('ultimoCambio viene del updatedAt local y el quién es el capturista ("app" si no hay nombre)', () => { const capturas = [fila({ leadId: 'loc_l1', board: 'LEADS', updatedAt: new Date('2026-09-05T08:00:00') })] const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA) expect(r.misLeads[0]?.ultimoCambio).toEqual({ fechaISO: '2026-09-05T08:00:00.000Z', usuario: 'Martin Zavala', detalle: 'Actualización', }) const rSinNombre = resumenMiDiaLocal(sinNombre, capturas, [], AHORA) expect(rSinNombre.misLeads[0]?.ultimoCambio?.usuario).toBe('app') expect(rSinNombre.sinMapeoMonday).toBe(true) }) it('la unidad del apartado sale del Cliente ligado; estadoApartado de Mis clientes cruza con el apartado local', () => { const capturas = [fila({ leadId: 'loc_a1', board: 'APARTADO', etapa: 'Pago Apartado' })] const clientes = [ cliente({ id: 'c1', mondayApartadoId: 'loc_a1', unidadCodigo: 'SENDA-101', proyecto: 'SENDA' }), cliente({ id: 'c2' }), ] const r = resumenMiDiaLocal(vendedor, capturas, clientes, AHORA) expect(r.misApartados[0]?.unidadCodigo).toBe('SENDA-101') const c1 = r.misClientes.find((c) => c.id === 'c1') expect(c1).toMatchObject({ completado: true, estadoApartado: 'Pago Apartado' }) expect(r.misClientes.find((c) => c.id === 'c2')?.estadoApartado).toBeNull() }) it('leadsNuevosSemana cuenta capturas de LEADS + OPORTUNIDADES de los últimos 7 días', () => { const capturas = [ fila({ leadId: 'loc_l1', board: 'LEADS', createdAt: new Date('2026-09-04T00:00:00') }), fila({ leadId: 'loc_o1', board: 'OPORTUNIDADES', createdAt: new Date('2026-09-01T00:00:00') }), fila({ leadId: 'loc_o2', board: 'OPORTUNIDADES', createdAt: new Date('2026-08-20T00:00:00') }), // > 7 días fila({ leadId: 'loc_a1', board: 'APARTADO', createdAt: new Date('2026-09-04T00:00:00') }), // apartado no cuenta ] const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA) expect(r.conteos.leadsNuevosSemana).toBe(2) }) it('misActividadesHoy muestra actividades abiertas con fecha <= mañana', () => { const actividades: ActividadLocalMiDia[] = [ { id: 'a1', titulo: 'Llamar a Juan', descripcion: null, usuarioId: 'u1', leadCapturaId: null, fechaVencimiento: new Date('2026-09-07T10:00:00'), fechaCierre: null, estado: 'ABIERTA', tipo: 'SEGUIMIENTO' }, { id: 'a2', titulo: 'Reunión passada', descripcion: null, usuarioId: 'u1', leadCapturaId: null, fechaVencimiento: new Date('2026-09-08T14:00:00'), fechaCierre: null, estado: 'ABIERTA', tipo: 'REUNION' }, { id: 'a3', titulo: 'Cerrada', descripcion: null, usuarioId: 'u1', leadCapturaId: null, fechaVencimiento: new Date('2026-09-07T09:00:00'), fechaCierre: new Date(), estado: 'CERRADA', tipo: 'SEGUIMIENTO' }, { id: 'a4', titulo: 'Sin fecha', descripcion: null, usuarioId: 'u1', leadCapturaId: null, fechaVencimiento: null, fechaCierre: null, estado: 'ABIERTA', tipo: 'SEGUIMIENTO' }, { id: 'a5', titulo: 'Lejana', descripcion: null, usuarioId: 'u1', leadCapturaId: null, fechaVencimiento: new Date('2026-09-15T10:00:00'), fechaCierre: null, estado: 'ABIERTA', tipo: 'SEGUIMIENTO' }, ] const r = resumenMiDiaLocal(vendedor, [], [], AHORA, actividades) // Solo a1 (hoy), a2 (mañana), a4 (sin fecha) — no a3 (cerrada), no a5 (lejana) expect(r.misActividadesHoy).toHaveLength(3) // Sort: fechaAsc, sin fecha al final expect(r.misActividadesHoy.map((a) => a.id)).toEqual(['a1', 'a2', 'a4']) expect(r.misActividadesHoy[0]).toMatchObject({ nombre: 'Llamar a Juan', tipo: 'SEGUIMIENTO' }) expect(r.conteos.actividadesAbiertas).toBe(4) // todas las ABIERTA (a1,a2,a4,a5) }) it('apartadosPorVencer usa fechaLimite de la captura', () => { const capturas = [ fila({ leadId: 'loc_a1', board: 'APARTADO', etapa: 'Pago Apartado', fechaLimite: new Date('2026-09-08') }), // +1 día fila({ leadId: 'loc_a2', board: 'APARTADO', etapa: 'Documentos', fechaLimite: new Date('2026-09-14') }), // +7 días fila({ leadId: 'loc_a3', board: 'APARTADO', etapa: 'Escrituracion', fechaLimite: new Date('2026-09-20') }), // +13 días: fuera fila({ leadId: 'loc_a4', board: 'APARTADO', etapa: 'Cancelado', fechaLimite: new Date('2026-09-08') }), // final: fuera fila({ leadId: 'loc_a5', board: 'APARTADO', etapa: 'Pago Apartado' }), // sin fecha: fuera ] const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA) expect(r.apartadosPorVencer).toHaveLength(2) expect(r.apartadosPorVencer.map((a) => a.id)).toEqual(['loc_a1', 'loc_a2']) expect(r.apartadosPorVencer[0]).toMatchObject({ diasRestantes: 1 }) expect(r.apartadosPorVencer[1]).toMatchObject({ diasRestantes: 7 }) }) it('unidadCodigo se extrae de datosJson de la oportunidad', () => { const capturas = [ fila({ leadId: 'loc_o1', board: 'OPORTUNIDADES', etapa: 'Afluencia', datosJson: JSON.stringify({ nombre: 'Test', unidadCodigo: 'SENDA-101' }), }), fila({ leadId: 'loc_o2', board: 'OPORTUNIDADES', etapa: 'Agendo Cita', datosJson: JSON.stringify({ nombre: 'Test2', desarrollo: 'DIAGONAL' }), }), ] const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA) expect(r.misOportunidades.items.find((o) => o.id === 'loc_o1')?.unidadCodigo).toBe('SENDA-101') expect(r.misOportunidades.items.find((o) => o.id === 'loc_o2')?.unidadCodigo).toBe('DIAGONAL') }) it('equipo se genera para coordinador con vendedores', () => { const usuarios: UsuarioLocal[] = [ { id: 'u1', nombre: 'Martin Zavala', rol: 'vendedor', activo: true }, { id: 'u2', nombre: 'Ana Perez', rol: 'vendedor', activo: true }, { id: 'u3', nombre: 'Inactivo', rol: 'vendedor', activo: false }, { id: 'u4', nombre: 'Coord', rol: 'coordinador', activo: true }, ] const capturas = [ fila({ leadId: 'loc_l1', board: 'LEADS', usuarioId: 'u1' }), fila({ leadId: 'loc_l2', board: 'LEADS', usuarioId: 'u1', etapa: 'Descartado' }), // terminal fila({ leadId: 'loc_l3', board: 'LEADS', usuarioId: 'u2', etapa: 'CONTACTADO', createdAt: new Date('2026-09-06T00:00:00') }), fila({ leadId: 'loc_o1', board: 'OPORTUNIDADES', usuarioId: 'u2' }), ] const r = resumenMiDiaLocal(coordinador, capturas, [], AHORA, [], usuarios) expect(r.equipo).toBeDefined() expect(r.equipo!.leadsNuevosSinAsignar).toBe(0) // todos asignados const porVendedor = r.equipo!.porVendedor expect(porVendedor).toHaveLength(2) // Martin: 1 lead activo (loc_l1), 2 leads nuevos semana (loc_l1 y loc_l2 ambos en semana) expect(porVendedor.find((v) => v.nombre === 'Martin Zavala')).toMatchObject({ mondayUserId: 0, leadsAsignados: 1, leadsNuevosSemana: 2 }) // Ana: 1 lead activo (loc_l3, 1 día atrás = semana), 1 oportunidad no cuenta en leads expect(porVendedor.find((v) => v.nombre === 'Ana Perez')).toMatchObject({ mondayUserId: 0, leadsAsignados: 1, leadsNuevosSemana: 1 }) }) it('equipo no se genera para vendedor', () => { const usuarios: UsuarioLocal[] = [ { id: 'u1', nombre: 'Martin', rol: 'vendedor', activo: true }, ] const r = resumenMiDiaLocal(vendedor, [], [], AHORA, [], usuarios) expect(r.equipo).toBeUndefined() }) }) // ---------- Unión (Monday vivo + pendientes locales) ---------- function resumenBase(): ResumenMiDia { return { generadoEn: AHORA.toISOString(), sinMapeoMonday: false, misLeads: [ { id: '9001', nombre: 'Lead Monday', telefono: '', email: '', estado: 'Nuevo', origen: 'Web', updatedAt: '2026-09-04T10:00:00.000Z', fuente: 'lead', ultimoCambio: null, }, ], misCapturas: [ { leadId: 'loc_n1', nombre: 'Cap Local', telefono: '', estado: '', origen: '', capturadoEn: '2026-09-03T10:00:00.000Z' }, { leadId: '9001', nombre: 'Lead Monday', telefono: '', estado: 'Nuevo', origen: 'Web', capturadoEn: '2026-09-02T10:00:00.000Z' }, ], misClientes: [], misActividadesHoy: [], apartadosPorVencer: [], misOportunidades: { porEtapa: { Afluencia: 1 }, items: [ { id: '9002', nombre: 'Op Monday', telefono: '', email: '', etapa: 'Afluencia', desarrollo: '', modeloInteres: '', unidadCodigo: '', updatedAt: '2026-09-04T09:00:00.000Z', ultimoCambio: null, }, ], }, misApartados: [ { id: '9003', nombre: 'Apt Monday', unidadCodigo: 'U-1', estado: 'Pago Apartado', fechaLimite: '2026-09-10', ultimoCambio: null }, ], conteos: { leadsNuevosSemana: 1, actividadesAbiertas: 0 }, } } describe('unirPendientesLocales (Monday vivo)', () => { it('agrega los items loc_* a la lista de su board con pendienteSync y re-ordena', () => { const pendientes = [ fila({ leadId: 'loc_n1', board: 'LEADS', etapa: 'NUEVO', updatedAt: new Date('2026-09-05T11:00:00') }), fila({ leadId: 'loc_n2', board: 'OPORTUNIDADES', etapa: 'Afluencia', updatedAt: new Date('2026-09-05T10:00:00') }), fila({ leadId: 'loc_n3', board: 'APARTADO', etapa: 'Pago Apartado' }), fila({ leadId: 'loc_n4', board: 'LEADS', etapa: 'Descartado' }), // terminal: no se agrega ] const r = unirPendientesLocales(resumenBase(), pendientes, 'Martin Zavala') // loc_n1 es más reciente que el de Monday: queda primero expect(r.misLeads.map((l) => l.id)).toEqual(['loc_n1', 'loc_n2', '9001']) expect(r.misLeads[0]).toMatchObject({ pendienteSync: true, fuente: 'lead' }) expect(r.misLeads[1]).toMatchObject({ pendienteSync: true, fuente: 'oportunidad' }) expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['loc_n2', '9002']) expect(r.misOportunidades.porEtapa).toEqual({ Afluencia: 2 }) expect(r.misApartados.map((a) => a.id)).toEqual(['9003', 'loc_n3']) // sin fecha límite al final expect(r.misApartados[1]).toMatchObject({ pendienteSync: true }) }) it('fila con mondayItemId (cambio pendiente sobre item Monday): no duplica, solo marca el existente', () => { const pendientes = [ fila({ leadId: '9001', board: 'LEADS', etapa: 'Contactado', mondayItemId: '9001' }), fila({ leadId: '9003', board: 'APARTADO', etapa: 'Documentos', mondayItemId: '9003' }), ] const r = unirPendientesLocales(resumenBase(), pendientes, 'Martin Zavala') expect(r.misLeads).toHaveLength(1) expect(r.misLeads[0]).toMatchObject({ id: '9001', pendienteSync: true }) expect(r.misApartados).toHaveLength(1) expect(r.misApartados[0]).toMatchObject({ id: '9003', pendienteSync: true }) }) it('marca Mis capturas y rellena el estado local cuando Monday aún no lo tiene', () => { const pendientes = [fila({ leadId: 'loc_n1', board: 'LEADS', etapa: 'NUEVO', origen: 'WhatsApp' })] const r = unirPendientesLocales(resumenBase(), pendientes, 'Martin Zavala') const captura = r.misCapturas.find((c) => c.leadId === 'loc_n1') expect(captura).toMatchObject({ pendienteSync: true, estado: 'NUEVO', origen: 'WhatsApp' }) // La captura ya conocida por Monday conserva su estado expect(r.misCapturas.find((c) => c.leadId === '9001')?.estado).toBe('Nuevo') }) })