214 lines
11 KiB
TypeScript
214 lines
11 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
resumenMiDiaLocal,
|
|
type ActividadLocalMiDia,
|
|
type ClienteLocalMiDia,
|
|
type FilaLocalMiDia,
|
|
type UsuarioLocal,
|
|
type UsuarioMiDia,
|
|
} from './mi-dia.service.js'
|
|
|
|
/**
|
|
* Mi día 100% local: resumen construido desde LeadCaptura, Cliente y
|
|
* ActividadLocal.
|
|
*/
|
|
|
|
const AHORA = new Date('2026-09-07T12:00:00')
|
|
|
|
const vendedor: UsuarioMiDia = { nombre: 'Martin Zavala', rol: 'vendedor' }
|
|
const coordinador: UsuarioMiDia = { nombre: 'Coord General', rol: 'coordinador' }
|
|
const sinNombre: UsuarioMiDia = { nombre: ' ', rol: 'vendedor' }
|
|
|
|
let consecutivo = 0
|
|
function fila(parcial: Partial<FilaLocalMiDia> & { 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<ClienteLocalMiDia> & { id: string }): ClienteLocalMiDia {
|
|
return {
|
|
nombreCompleto: `Cliente ${parcial.id}`,
|
|
proyecto: '',
|
|
unidadCodigo: null,
|
|
mondayApartadoId: null,
|
|
createdAt: new Date('2026-09-04T10:00:00'),
|
|
...parcial,
|
|
}
|
|
}
|
|
|
|
describe('resumenMiDiaLocal', () => {
|
|
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.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)
|
|
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'])
|
|
// "Mis capturas" son sólo altas del tablero Leads que no aparezcan ya en
|
|
// otro tab: loc_l1 ya está en misLeads y loc_l2 (Descartado) es la única
|
|
// que queda; las filas de OPORTUNIDADES y APARTADO ya no se repiten aquí.
|
|
expect(r.misCapturas.map((c) => c.leadId)).toEqual(['loc_l2'])
|
|
// 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 REAL de la fila (no el que ve)', () => {
|
|
const capturas = [fila({ leadId: 'loc_l1', board: 'LEADS', updatedAt: new Date('2026-09-05T08:00:00') })]
|
|
// El capturista de la fila (u1) es otro usuario distinto del que consulta
|
|
const usuarios = [{ id: 'u1', nombre: 'Martín Zavala', rol: 'vendedor', activo: true }]
|
|
const r = resumenMiDiaLocal(vendedor, capturas, [], AHORA, [], usuarios)
|
|
expect(r.misLeads[0]?.ultimoCambio).toEqual({
|
|
fechaISO: '2026-09-05T08:00:00.000Z',
|
|
usuario: 'Martín Zavala',
|
|
detalle: 'Actualización',
|
|
})
|
|
|
|
// Sin la lista de usuarios (o el capturista fuera de ella) → 'app'
|
|
const rSinUsuarios = resumenMiDiaLocal(vendedor, capturas, [], AHORA)
|
|
expect(rSinUsuarios.misLeads[0]?.ultimoCambio?.usuario).toBe('app')
|
|
|
|
const rSinNombre = resumenMiDiaLocal(sinNombre, capturas, [], AHORA)
|
|
expect(rSinNombre.misLeads[0]?.ultimoCambio?.usuario).toBe('app')
|
|
})
|
|
|
|
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({ usuarioId: 'u1', 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({ usuarioId: 'u2', 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()
|
|
})
|
|
})
|