feat(mejoras): 14 mejoras al CRM Frasa
Fase 1 - UI: dark mode, skeletons, toasts Fase 2 - Filtros: avanzados Mi día, búsqueda global Ctrl+K Fase 3 - Datos: export Excel, notas apartados, auditoría Fase 4 - Dashboard: gráficas vendedor + métricas uso Fase 5 - Responsive: tablets optimizadas Fase 6 - PWA: IndexedDB offline, push notifications Backend: 3 tablas nuevas, 6 services, 6 endpoints nuevos 721 API tests + fronts verdes.
This commit is contained in:
147
api-gateway/src/services/reporte-mi-tablero.service.test.ts
Normal file
147
api-gateway/src/services/reporte-mi-tablero.service.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { construirMiTablero } from './reporte-mi-tablero.service.js'
|
||||
|
||||
// ---------- Fixtures ----------
|
||||
|
||||
function captura(
|
||||
overrides: Partial<{
|
||||
leadId: string
|
||||
board: string
|
||||
etapa: string
|
||||
origen: string | null
|
||||
desarrollo: string | null
|
||||
createdAt: Date
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
leadId: overrides.leadId ?? `cap-${Math.random().toString(36).slice(2, 8)}`,
|
||||
board: overrides.board ?? 'LEADS',
|
||||
etapa: overrides.etapa ?? 'NUEVO',
|
||||
origen: overrides.origen ?? 'Facebook',
|
||||
desarrollo: overrides.desarrollo ?? 'DIAGONAL',
|
||||
createdAt: overrides.createdAt ?? new Date('2026-09-05T10:00:00'),
|
||||
}
|
||||
}
|
||||
|
||||
function cotizacion(
|
||||
overrides: Partial<{ precioFinal: number; vendedorId: string | null; createdAt: Date }> = {},
|
||||
) {
|
||||
return {
|
||||
precioFinal: overrides.precioFinal ?? 850000,
|
||||
vendedorId: overrides.vendedorId ?? 'u-1',
|
||||
createdAt: overrides.createdAt ?? new Date('2026-09-01T12:00:00'),
|
||||
}
|
||||
}
|
||||
|
||||
function actividad(
|
||||
overrides: Partial<{ usuarioId: string; estado: string; fechaVencimiento: Date | null }> = {},
|
||||
) {
|
||||
return {
|
||||
usuarioId: overrides.usuarioId ?? 'u-1',
|
||||
estado: overrides.estado ?? 'ABIERTA',
|
||||
fechaVencimiento: overrides.fechaVencimiento ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
const AHORA = new Date('2026-09-07T12:00:00')
|
||||
|
||||
// ---------- construirMiTablero (puro) ----------
|
||||
|
||||
describe('construirMiTablero', () => {
|
||||
it('genera 30 entradas en leadsPorDia con ceros para días sin leads', () => {
|
||||
const tablero = construirMiTablero([], [], [], AHORA)
|
||||
expect(tablero.leadsPorDia.length).toBe(30)
|
||||
// Todos los días deben tener cantidad 0
|
||||
expect(tablero.leadsPorDia.every((d) => d.cantidad === 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('cuenta leads por día correctamente', () => {
|
||||
const hace2Dias = new Date(AHORA.getTime() - 2 * 24 * 60 * 60 * 1000)
|
||||
const capturas = [
|
||||
captura({ createdAt: hace2Dias }),
|
||||
captura({ createdAt: hace2Dias }),
|
||||
captura({ createdAt: hace2Dias }),
|
||||
]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
const entrada = tablero.leadsPorDia.find((d) => d.cantidad > 0)
|
||||
expect(entrada).toBeDefined()
|
||||
expect(entrada!.cantidad).toBe(3)
|
||||
})
|
||||
|
||||
it('cuenta conversión correctamente', () => {
|
||||
const capturas = [
|
||||
captura({ board: 'LEADS' }),
|
||||
captura({ board: 'LEADS' }),
|
||||
captura({ board: 'OPORTUNIDADES' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE (VENTA)' }),
|
||||
captura({ board: 'APARTADO', etapa: 'CANCELADO' }),
|
||||
]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
expect(tablero.conversion.leads).toBe(2)
|
||||
expect(tablero.conversion.oportunidades).toBe(1)
|
||||
expect(tablero.conversion.apartados).toBe(1) // CANCELADO no cuenta
|
||||
expect(tablero.conversion.vendidos).toBe(1)
|
||||
})
|
||||
|
||||
it('calcula ventas del mes correctamente', () => {
|
||||
const cots = [
|
||||
cotizacion({ precioFinal: 850000, createdAt: new Date('2026-09-01T12:00:00') }),
|
||||
cotizacion({ precioFinal: 750000, createdAt: new Date('2026-09-05T12:00:00') }),
|
||||
cotizacion({ precioFinal: 900000, createdAt: new Date('2026-08-15T12:00:00') }), // mes anterior
|
||||
]
|
||||
const tablero = construirMiTablero([], cots, [], AHORA)
|
||||
expect(tablero.ventasMes.unidades).toBe(2)
|
||||
expect(tablero.ventasMes.monto).toBe(1600000) // 850000 + 750000
|
||||
})
|
||||
|
||||
it('agrupa por origen con conteo de convertidos', () => {
|
||||
const capturas = [
|
||||
captura({ board: 'LEADS', origen: 'Facebook' }),
|
||||
captura({ board: 'LEADS', origen: 'Facebook' }),
|
||||
captura({ board: 'OPORTUNIDADES', origen: 'Facebook' }),
|
||||
captura({ board: 'LEADS', origen: 'Referido' }),
|
||||
]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
const fb = tablero.porOrigen.find((o) => o.origen === 'Facebook')
|
||||
expect(fb).toBeDefined()
|
||||
expect(fb!.total).toBe(3)
|
||||
expect(fb!.convertidos).toBe(1)
|
||||
const ref = tablero.porOrigen.find((o) => o.origen === 'Referido')
|
||||
expect(ref).toBeDefined()
|
||||
expect(ref!.total).toBe(1)
|
||||
expect(ref!.convertidos).toBe(0)
|
||||
})
|
||||
|
||||
it('agrupa por desarrollo solo con vendidos', () => {
|
||||
const capturas = [
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE (VENTA)', desarrollo: 'DIAGONAL' }),
|
||||
captura({ board: 'APARTADO', etapa: 'ESCRITURADO', desarrollo: 'DIAGONAL' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE (VENTA)', desarrollo: 'ATELIER' }),
|
||||
captura({ board: 'APARTADO', etapa: 'NUEVO', desarrollo: 'SENDA' }), // no vendido
|
||||
]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
expect(tablero.porDesarrollo).toEqual([
|
||||
{ desarrollo: 'DIAGONAL', ventas: 2 },
|
||||
{ desarrollo: 'ATELIER', ventas: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('cuenta actividades hoy y pendientes', () => {
|
||||
const actividades = [
|
||||
actividad({ estado: 'ABIERTA', fechaVencimiento: new Date('2026-09-07T10:00:00') }),
|
||||
actividad({ estado: 'ABIERTA', fechaVencimiento: new Date('2026-09-08T10:00:00') }),
|
||||
actividad({ estado: 'ABIERTA' }),
|
||||
actividad({ estado: 'CERRADA' }),
|
||||
]
|
||||
const tablero = construirMiTablero([], [], actividades, AHORA)
|
||||
expect(tablero.actividadesHoy).toBe(2) // 1 vencida + 1 sin fecha (hoy)
|
||||
expect(tablero.actividadesPendientes).toBe(3) // 3 ABIERTA
|
||||
})
|
||||
|
||||
it('normaliza orígenes vacíos a (sin origen)', () => {
|
||||
const capturas = [captura({ board: 'LEADS', origen: '' })]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
expect(tablero.porOrigen.length).toBe(1)
|
||||
expect(tablero.porOrigen[0].origen).toBe('(sin origen)')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user