feat: roles con modulos por rol + mejoras Mi dia y descarte
- Matriz de roles (vendedor, administracion_venta, director_ventas, escrituracion, direccion_general, admin) con modulos visibles por rol; auth/me y login incluyen 'modulos'; PATCH /usuarios/:id/rol (admin). - Landing: Cobranza -> Clientes, Chat IA -> Chat Hermes, nuevo modulo Contactos (vista interna leyendo el tablero Contactos), grid filtrado por rol, rol visible en Configuracion de cuenta. - Dashboards: tab 'Tablero comercial' y tabs filtrados por rol. - Boton '<- Menu' en el header de todas las apps. - Mi dia: cada lead muestra el ultimo cambio (que, quien, cuando) desde el activity log de Monday (normalizacion de created_at 100ns/us/ms). - Descarte: el motivo se crea en el grupo de su categoria en el tablero Motivos No Compra.
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
detalleEventoUltimoCambio,
|
||||
esEstadoFinalApartado,
|
||||
esEstadoTerminalLead,
|
||||
esEtapaTerminal,
|
||||
resumenMiDia,
|
||||
type UsuarioMiDia,
|
||||
} from './mi-dia.service.js'
|
||||
import type { ActividadMonday, ApartadoMonday, LeadMonday, OportunidadMonday } from './monday.service.js'
|
||||
import type {
|
||||
ActividadMonday,
|
||||
ApartadoMonday,
|
||||
LeadMonday,
|
||||
OportunidadMonday,
|
||||
UltimoCambioMonday,
|
||||
} from './monday.service.js'
|
||||
|
||||
const MI_ID = 106736727
|
||||
const OTRO_ID = 111111111
|
||||
@@ -71,6 +78,8 @@ function mondayMock(datos: {
|
||||
oportunidades?: OportunidadMonday[]
|
||||
actividades?: ActividadMonday[]
|
||||
apartados?: ApartadoMonday[]
|
||||
/** boardId → (itemId → último cambio); boards de test: Leads '444', Oportunidades '555' */
|
||||
ultimosCambios?: Record<string, Map<string, UltimoCambioMonday>>
|
||||
}) {
|
||||
return {
|
||||
async getLeads() {
|
||||
@@ -85,6 +94,9 @@ function mondayMock(datos: {
|
||||
async getApartados() {
|
||||
return datos.apartados ?? []
|
||||
},
|
||||
async getUltimosCambios(boardId: string, _itemIds: string[]) {
|
||||
return datos.ultimosCambios?.[boardId] ?? new Map<string, UltimoCambioMonday>()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +130,63 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
|
||||
expect(r.misLeads[0]?.telefono).toBe('4771234567')
|
||||
})
|
||||
|
||||
it('ultimoCambio: último evento por item, agrupando ids por fuente y resolviendo el nombre', async () => {
|
||||
const monday = mondayMock({
|
||||
leads: [lead({ id: 'l1', updatedAt: '2026-08-28T08:00:00Z' })],
|
||||
oportunidades: [oportunidad({ id: 'o1', updatedAt: '2026-08-27T08:00:00Z' })],
|
||||
ultimosCambios: {
|
||||
'444': new Map([['l1', { fechaISO: '2026-08-28T10:00:00.000Z', usuarioId: MI_ID, evento: 'update_status' }]]),
|
||||
'555': new Map([['o1', { fechaISO: '2026-08-28T09:00:00.000Z', usuarioId: OTRO_ID, evento: 'create_pulse' }]]),
|
||||
},
|
||||
})
|
||||
const nombres = new Map([[MI_ID, 'Martin Zavala']]) // OTRO_ID no está mapeado
|
||||
const r = await resumenMiDia(monday, vendedor, nombres, [], [], AHORA)
|
||||
expect(r.misLeads.find((l) => l.id === 'l1')?.ultimoCambio).toEqual({
|
||||
fechaISO: '2026-08-28T10:00:00.000Z',
|
||||
usuario: 'Martin Zavala',
|
||||
detalle: 'Actualización',
|
||||
})
|
||||
// usuarioId sin nombre en el mapa → fallback null
|
||||
expect(r.misLeads.find((l) => l.id === 'o1')?.ultimoCambio).toEqual({
|
||||
fechaISO: '2026-08-28T09:00:00.000Z',
|
||||
usuario: null,
|
||||
detalle: 'Alta',
|
||||
})
|
||||
})
|
||||
|
||||
it('ultimoCambio: null cuando el item no tiene eventos en el activity log', async () => {
|
||||
const monday = mondayMock({
|
||||
leads: [lead({ id: 'l1' })],
|
||||
ultimosCambios: { '444': new Map() }, // sin eventos
|
||||
})
|
||||
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
||||
expect(r.misLeads[0]?.ultimoCambio).toBeNull()
|
||||
})
|
||||
|
||||
it('ultimoCambio: usuarioId null (evento de automatización) → usuario null', async () => {
|
||||
const monday = mondayMock({
|
||||
leads: [lead({ id: 'l1' })],
|
||||
ultimosCambios: {
|
||||
'444': new Map([['l1', { fechaISO: '2026-08-28T10:00:00.000Z', usuarioId: null, evento: 'move_to_group' }]]),
|
||||
},
|
||||
})
|
||||
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
||||
expect(r.misLeads[0]?.ultimoCambio).toEqual({
|
||||
fechaISO: '2026-08-28T10:00:00.000Z',
|
||||
usuario: null,
|
||||
detalle: 'Cambio de grupo',
|
||||
})
|
||||
})
|
||||
|
||||
it('detalleEventoUltimoCambio: humaniza los eventos conocidos y pasa el crudo si no', () => {
|
||||
expect(detalleEventoUltimoCambio('create_pulse')).toBe('Alta')
|
||||
expect(detalleEventoUltimoCambio('create_update')).toBe('Nota agregada')
|
||||
expect(detalleEventoUltimoCambio('update_column_value')).toBe('Actualización')
|
||||
expect(detalleEventoUltimoCambio('update_status')).toBe('Actualización')
|
||||
expect(detalleEventoUltimoCambio('move_to_group')).toBe('Cambio de grupo')
|
||||
expect(detalleEventoUltimoCambio('update_name')).toBe('update_name')
|
||||
})
|
||||
|
||||
it('misOportunidades: conteo de MIS oportunidades por etapa (incluye terminales)', async () => {
|
||||
const monday = mondayMock({
|
||||
oportunidades: [
|
||||
|
||||
Reference in New Issue
Block a user