feat(ux): 17 mejoras de experiencia de usuario

Backend (10 endpoints nuevos):
- Recordatorios personales (CRUD + tabla RecordatorioVendedor)
- Lead scoring 0-100 (etapa+recencia+datos)
- Dashboard mora (pagos vencidos, monto, días atraso)
- Timeline de pagos (pagado/vence/futuro)
- Perfil completo contacto (pagos+cotizaciones+docs+score)
- Exportar contactos a CSV

Frontend Mi día:
- Resumen matutino (saludo+KPIs)
- Historial visual lead (timeline)
- Score badge 0-100 (verde/amarillo/rojo)
- Vista kanban (drag & drop)

Frontend Cobranza:
- Dashboard mora (tabla+WhatsApp)
- Timeline pagos visual (nodos coloreados)

Frontend Alta Clientes:
- Checklist documentos visual
- Foto cliente (cámara)

Frontend Landing:
- Perfil contacto completo
- Exportar contactos CSV

Frontend Mi día (extras):
- Comparativa mensual
- KPIs dashboard (leads hoy, ventas, conversión)

806 tests, 5 apps construidas.
This commit is contained in:
2026-09-08 07:42:54 +00:00
parent 5063ffe281
commit db18685872
34 changed files with 2950 additions and 33 deletions

View File

@@ -0,0 +1,143 @@
import { describe, expect, it, vi, beforeEach } from 'vitest'
const prismaMock = vi.hoisted(() => ({
leadCaptura: {
findMany: vi.fn(),
},
}))
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
import { calcularScore, obtenerScores } from './lead-scoring.service.js'
describe('calcularScore', () => {
const ahora = new Date('2026-09-08T12:00:00Z')
it('asigna score base según etapa (NUEVO = 20)', () => {
const lead = {
etapa: 'NUEVO',
telefono: null,
email: null,
origen: null,
createdAt: new Date('2026-09-07T12:00:00Z'),
updatedAt: new Date('2026-09-07T12:00:00Z'),
}
// 20 (etapa) + 5 (recencia <7d) = 25
expect(calcularScore(lead, ahora)).toBe(25)
})
it('asigna score CALIFICADO (90)', () => {
const lead = {
etapa: 'CALIFICADO',
telefono: '4771234567',
email: 'test@email.com',
origen: 'Facebook',
createdAt: new Date('2026-09-08T00:00:00Z'),
updatedAt: new Date('2026-09-08T00:00:00Z'),
}
// 90 + 10 (<24h) + 5 tel + 5 email + 5 origen = 115 → clamp 100
expect(calcularScore(lead, ahora)).toBe(100)
})
it('aplica penalización >30 días sin actividad', () => {
const lead = {
etapa: 'CONTACTADO',
telefono: null,
email: null,
origen: null,
createdAt: new Date('2026-07-01T00:00:00Z'),
updatedAt: new Date('2026-07-01T00:00:00Z'),
}
// 40 (CONTACTADO) + (-20) (>30d) = 20
expect(calcularScore(lead, ahora)).toBe(20)
})
it('score mínimo es 0', () => {
const lead = {
etapa: 'DESCARTADO',
telefono: null,
email: null,
origen: null,
createdAt: new Date('2026-01-01T00:00:00Z'),
updatedAt: new Date('2026-01-01T00:00:00Z'),
}
// 10 (desconocida) + (-20) = -10 → clamp 0
expect(calcularScore(lead, ahora)).toBe(0)
})
it('recencia <24h suma +10', () => {
const lead = {
etapa: 'NUEVO',
telefono: '1234567890',
email: 'a@b.com',
origen: 'Web',
createdAt: new Date('2026-09-08T10:00:00Z'),
updatedAt: new Date('2026-09-08T10:00:00Z'),
}
// 20 + 10 + 5 + 5 + 5 = 45
expect(calcularScore(lead, ahora)).toBe(45)
})
it('INTERESADO = 60 + recencia <7d = +5 = 65', () => {
const lead = {
etapa: 'INTERESADO',
telefono: null,
email: null,
origen: null,
createdAt: new Date('2026-09-05T12:00:00Z'),
updatedAt: new Date('2026-09-05T12:00:00Z'),
}
expect(calcularScore(lead, ahora)).toBe(65)
})
})
describe('obtenerScores', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('devuelve scores ordenados de mayor a menor', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
{
leadId: 'l1',
nombreLead: 'A',
usuarioId: 'u1',
etapa: 'NUEVO',
telefono: null,
email: null,
origen: null,
createdAt: new Date('2026-09-07'),
updatedAt: new Date('2026-09-07'),
},
{
leadId: 'l2',
nombreLead: 'B',
usuarioId: 'u1',
etapa: 'CALIFICADO',
telefono: '1234567890',
email: 'b@b.com',
origen: 'Web',
createdAt: new Date('2026-09-08'),
updatedAt: new Date('2026-09-08'),
},
])
const scores = await obtenerScores()
expect(scores).toHaveLength(2)
expect(scores[0]!.leadId).toBe('l2')
expect(scores[0]!.score).toBeGreaterThan(scores[1]!.score)
})
it('filtra por usuarioId cuando se proporciona', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([])
await obtenerScores('u1')
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalledWith({
where: {
board: { in: ['LEADS', 'OPORTUNIDADES'] },
usuarioId: 'u1',
},
})
})
})