feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday

Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario
Mi día: modal español real, cita desde modal, score falso eliminado
Cotizador: barra sticky, persistencia local con folio
Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas
Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp
Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado
Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas
Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones

69 archivos, 7 apps construidas
This commit is contained in:
2026-09-11 06:52:37 +00:00
parent 075d47c304
commit 64b01246d6
69 changed files with 1600 additions and 1133 deletions

View File

@@ -12,9 +12,6 @@ const prismaMock = vi.hoisted(() => ({
leadCaptura: {
findMany: vi.fn(),
},
leadOportunidad: {
findMany: vi.fn(),
},
cliente: {
findMany: vi.fn(),
},
@@ -36,7 +33,6 @@ vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
import { resumenMiDiaEndpoint } from './mi-dia.controller.js'
import { saludMonday } from '../services/salud-monday.service.js'
const usuarioFake = {
id: 'u-1',
@@ -80,12 +76,9 @@ const reqFake = { usuarioToken: { sub: 'u-1' } } as unknown as Request
beforeEach(() => {
vi.clearAllMocks()
vi.restoreAllMocks() // restaura los spyOn(saludMonday, 'estaDisponible')
saludMonday.restablecer()
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake)
prismaMock.usuario.findMany.mockResolvedValue([])
prismaMock.leadCaptura.findMany.mockResolvedValue([])
prismaMock.leadOportunidad.findMany.mockResolvedValue([])
prismaMock.cliente.findMany.mockResolvedValue([])
prismaMock.actividadLocal.findMany.mockResolvedValue([])
mondayMock.getUsuariosWorkspace.mockResolvedValue([])
@@ -96,9 +89,8 @@ beforeEach(() => {
mondayMock.getUltimosCambios.mockResolvedValue(new Map())
})
describe('resumenMiDiaEndpoint (modo local y unión)', () => {
it('Monday caído (circuit breaker): resumen 100% local, sin llamadas a Monday', async () => {
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
describe('resumenMiDiaEndpoint (siempre local, Monday purgado)', () => {
it('responde 100% desde LeadCaptura: Monday no se llama nunca', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
capturaFake({ leadId: 'loc_l1', board: 'LEADS', etapa: 'NUEVO', pendienteSync: true }),
capturaFake({ leadId: 'loc_o1', board: 'OPORTUNIDADES', etapa: 'Afluencia', pendienteSync: true }),
@@ -109,15 +101,16 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
expect(mondayMock.getLeads).not.toHaveBeenCalled()
expect(mondayMock.getOportunidades).not.toHaveBeenCalled()
expect(mondayMock.getApartados).not.toHaveBeenCalled()
expect(mondayMock.getUsuariosWorkspace).not.toHaveBeenCalled()
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
expect((resumen["misLeads"] as unknown[]).length).toBe(1) // solo leads, no oportunidades
expect((resumen['misLeads'] as unknown[]).length).toBe(1) // solo leads, no oportunidades
expect((resumen['misOportunidades'] as { items: unknown[] }).items).toHaveLength(1)
expect(resumen['misActividadesHoy']).toEqual([])
expect(resumen['conteos']).toMatchObject({ actividadesAbiertas: 0 })
})
it('Monday se cae a media lectura (502): cae al resumen local', async () => {
mondayMock.getLeads.mockRejectedValue(new AppError(502, 'Monday no respondió'))
it('las capturas locales llegan con pendienteSync (badge ⏳ del front)', async () => {
prismaMock.leadCaptura.findMany.mockResolvedValue([
capturaFake({ leadId: 'loc_l1', board: 'LEADS', pendienteSync: true }),
])
@@ -126,41 +119,11 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
await resumenMiDiaEndpoint(reqFake, res)
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
expect((resumen['misLeads'] as { id: string }[]).map((l) => l.id)).toEqual(['loc_l1'])
expect((resumen['misLeads'] as { id: string; pendienteSync?: boolean }[]).map((l) => l.id)).toEqual(['loc_l1'])
expect((resumen['misLeads'] as { pendienteSync?: boolean }[])[0]?.pendienteSync).toBe(true)
})
it('Monday vivo: unión — las capturas pendientes se agregan con pendienteSync (badge ⏳)', async () => {
mondayMock.getLeads.mockResolvedValue([
{
id: '9001',
nombre: 'Lead Monday',
createdAt: '2026-09-01T10:00:00.000Z',
updatedAt: '2026-09-02T10:00:00.000Z',
estado: 'Nuevo',
origen: 'Web',
telefono: '',
email: '',
vendedorIds: [123],
},
])
prismaMock.leadCaptura.findMany.mockResolvedValue([
capturaFake({ leadId: 'loc_n1', board: 'LEADS', etapa: 'NUEVO', pendienteSync: true, updatedAt: new Date('2026-09-05T10:00:00') }),
])
const { res, json } = resFake()
await resumenMiDiaEndpoint(reqFake, res)
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
const leads = resumen['misLeads'] as { id: string; pendienteSync?: boolean }[]
expect(leads.map((l) => l.id)).toEqual(['loc_n1', '9001']) // el local, más reciente, primero
expect(leads[0]?.pendienteSync).toBe(true)
expect(leads[1]?.pendienteSync).toBeUndefined() // el de Monday no lleva flag
// Y la captura quedó marcada en Mis capturas
const capturas = resumen['misCapturas'] as { leadId: string; pendienteSync?: boolean; estado: string }[]
expect(capturas.find((c) => c.leadId === 'loc_n1')).toMatchObject({ pendienteSync: true, estado: 'NUEVO' })
})
it('Monday vivo y sin pendientes: respuesta idéntica a la de hoy (sin flags)', async () => {
it('sin capturas: listas vacías y sin flags de sync', async () => {
const { res, json } = resFake()
await resumenMiDiaEndpoint(reqFake, res)
@@ -170,8 +133,20 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
expect(JSON.stringify(resumen)).not.toContain('pendienteSync')
})
it('un error no-Monday se propaga (no se traga nada)', async () => {
mondayMock.getLeads.mockRejectedValue(new AppError(500, 'bug'))
it('los usuarios del equipo solo se consultan para roles coordinador/admin/corporativo', async () => {
await resumenMiDiaEndpoint(reqFake, resFake().res)
expect(prismaMock.usuario.findMany).not.toHaveBeenCalled()
prismaMock.usuario.findUnique.mockResolvedValue({ ...usuarioFake, rol: 'coordinador' })
await resumenMiDiaEndpoint(reqFake, resFake().res)
expect(prismaMock.usuario.findMany).toHaveBeenCalledWith({
where: { activo: true },
select: { id: true, nombre: true, rol: true, activo: true },
})
})
it('un error de BD se propaga (no se traga nada)', async () => {
prismaMock.leadCaptura.findMany.mockRejectedValue(new AppError(500, 'bug'))
await expect(resumenMiDiaEndpoint(reqFake, resFake().res)).rejects.toThrow('bug')
})
})