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:
2026-09-07 06:04:49 +00:00
parent 388e1da338
commit c77b4c0628
170 changed files with 5802 additions and 637 deletions

View 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)')
})
})