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:
47
api-gateway/src/controllers/contactos.controller.test.ts
Normal file
47
api-gateway/src/controllers/contactos.controller.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Request, Response } from 'express'
|
||||
import { listarContactosEndpoint } from './contactos.controller.js'
|
||||
|
||||
// ---------- Monday mockeado (los tests no tocan la red) ----------
|
||||
|
||||
const mondayMock = vi.hoisted(() => ({ getContactos: vi.fn() }))
|
||||
|
||||
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
|
||||
|
||||
const CONTACTOS = [
|
||||
{
|
||||
id: '1',
|
||||
nombre: 'JUAN PÉREZ',
|
||||
telefono: '5512345678',
|
||||
email: 'juan@correo.mx',
|
||||
estado: 'CLIENTE',
|
||||
updatedAt: '2026-09-01T10:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
function resFake() {
|
||||
const json = vi.fn()
|
||||
const status = vi.fn().mockReturnValue({ json })
|
||||
return { res: { json, status } as unknown as Response, status, json }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mondayMock.getContactos.mockResolvedValue(CONTACTOS)
|
||||
})
|
||||
|
||||
describe('listarContactosEndpoint (GET /api/contactos)', () => {
|
||||
it('devuelve los contactos del tablero de Monday', async () => {
|
||||
const { res, json } = resFake()
|
||||
|
||||
await listarContactosEndpoint({} as Request, res)
|
||||
|
||||
expect(json).toHaveBeenCalledWith(CONTACTOS)
|
||||
})
|
||||
|
||||
it('propaga el error si Monday falla (el error handler responde 5xx)', async () => {
|
||||
mondayMock.getContactos.mockRejectedValue(new Error('Monday caído'))
|
||||
const { res } = resFake()
|
||||
await expect(listarContactosEndpoint({} as Request, res)).rejects.toThrow('Monday caído')
|
||||
})
|
||||
})
|
||||
10
api-gateway/src/controllers/contactos.controller.ts
Normal file
10
api-gateway/src/controllers/contactos.controller.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { mondayService } from '../services/monday.service.js'
|
||||
|
||||
/**
|
||||
* GET /api/contactos (Bearer) → contactos del tablero Contactos de Monday
|
||||
* (nombre, teléfono, email, estado), lo más reciente primero, máx 100.
|
||||
*/
|
||||
export async function listarContactosEndpoint(_req: Request, res: Response): Promise<void> {
|
||||
res.json(await mondayService.getContactos())
|
||||
}
|
||||
@@ -26,6 +26,18 @@ export async function resumenMiDiaEndpoint(req: Request, res: Response): Promise
|
||||
mapeados.filter((m) => m.mondayUserId !== null).map((m) => [m.mondayUserId!, m.nombre]),
|
||||
)
|
||||
|
||||
// TODOS los usuarios del workspace de Monday (el "último cambio" de un lead
|
||||
// puede ser de alguien sin cuenta en el ecosistema); si ya estaba mapeado
|
||||
// gana el nombre del ecosistema. Si Monday falla, el resumen sale igual.
|
||||
try {
|
||||
for (const u of await mondayService.getUsuariosWorkspace()) {
|
||||
if (!nombresPorMondayId.has(u.id)) nombresPorMondayId.set(u.id, u.nombre)
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('No se pudieron listar los usuarios del workspace de Monday:', err)
|
||||
}
|
||||
|
||||
// Capturas de afluencia registradas por el usuario (registro local)
|
||||
const capturas = await prisma.leadCaptura.findMany({
|
||||
where: { usuarioId: usuario.id },
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Request, Response } from 'express'
|
||||
import type { Usuario } from '@prisma/client'
|
||||
import {
|
||||
actualizarRolEndpoint,
|
||||
actualizarVinculoMondayEndpoint,
|
||||
listarUsuariosMondayEndpoint,
|
||||
} from './usuarios.controller.js'
|
||||
@@ -91,6 +92,7 @@ describe('actualizarVinculoMondayEndpoint (PATCH /api/usuarios/me/monday)', () =
|
||||
rol: 'vendedor',
|
||||
asignacion: 'MONTEBELLO',
|
||||
mondayUserId: 13676,
|
||||
modulos: ['mi-dia', 'alta-cliente', 'alta-afluencia', 'cotizador', 'mi-tablero', 'contactos', 'chat'],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,3 +139,57 @@ describe('actualizarVinculoMondayEndpoint (PATCH /api/usuarios/me/monday)', () =
|
||||
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('actualizarRolEndpoint (PATCH /api/usuarios/:id/rol)', () => {
|
||||
function reqAdmin(overrides: Partial<Request> = {}): Request {
|
||||
return {
|
||||
body: { rol: 'escrituracion' },
|
||||
params: { id: 'u-9' },
|
||||
usuarioToken: { sub: 'u-1', username: 'admin', rol: 'admin' },
|
||||
...overrides,
|
||||
} as Request
|
||||
}
|
||||
|
||||
it('admin con rol válido: actualiza y devuelve el perfil con sus módulos', async () => {
|
||||
prismaMock.usuario.update.mockResolvedValue(usuarioFake({ id: 'u-9', rol: 'escrituracion' }))
|
||||
const { res, json } = resFake()
|
||||
|
||||
await actualizarRolEndpoint(reqAdmin(), res)
|
||||
|
||||
expect(prismaMock.usuario.update).toHaveBeenCalledWith({
|
||||
where: { id: 'u-9' },
|
||||
data: { rol: 'escrituracion' },
|
||||
})
|
||||
expect(json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ rol: 'escrituracion', modulos: expect.arrayContaining(['cobranza']) }),
|
||||
)
|
||||
})
|
||||
|
||||
it('sin rol admin: 403 y no toca la BD', async () => {
|
||||
const { res } = resFake()
|
||||
const error = await actualizarRolEndpoint(
|
||||
reqAdmin({ usuarioToken: { sub: 'u-2', username: 'x', rol: 'vendedor' } }),
|
||||
res,
|
||||
).catch((e: unknown) => e)
|
||||
expect(error).toMatchObject({ status: 403 })
|
||||
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rol inválido: 400 y no toca la BD', async () => {
|
||||
const { res } = resFake()
|
||||
const error = await actualizarRolEndpoint(reqAdmin({ body: { rol: 'coordinador' } }), res).catch(
|
||||
(e: unknown) => e,
|
||||
)
|
||||
expect(error).toMatchObject({ status: 400 })
|
||||
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sin token verificado: 400', async () => {
|
||||
const { res } = resFake()
|
||||
await expect(actualizarRolEndpoint(reqAdmin({ usuarioToken: undefined }), res)).rejects.toThrow(
|
||||
'Falta el token verificado',
|
||||
)
|
||||
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { z } from 'zod'
|
||||
import { mondayService } from '../services/monday.service.js'
|
||||
import { prisma } from '../services/prisma.js'
|
||||
import { aPublico } from '../services/auth.service.js'
|
||||
import { BadRequestError } from '../utils/errors.js'
|
||||
import { esRolValido } from '../config/roles.js'
|
||||
import { AppError, BadRequestError } from '../utils/errors.js'
|
||||
|
||||
/**
|
||||
* Vínculo usuario del ecosistema ↔ usuario de Monday (people). Es el mapeo
|
||||
@@ -42,3 +43,27 @@ export async function actualizarVinculoMondayEndpoint(req: Request, res: Respons
|
||||
})
|
||||
res.json(aPublico(usuario))
|
||||
}
|
||||
|
||||
const cambiarRolSchema = z.object({
|
||||
rol: z.string().min(1),
|
||||
})
|
||||
|
||||
/**
|
||||
* PATCH /api/usuarios/:id/rol (Bearer, solo admin) { rol } → usuario público
|
||||
* actualizado. Solo roles de la matriz nueva (config/roles.ts); los legacy
|
||||
* ('coordinador', 'corporativo') ya no se asignan.
|
||||
*/
|
||||
export async function actualizarRolEndpoint(req: Request, res: Response): Promise<void> {
|
||||
if (!req.usuarioToken) throw new BadRequestError('Falta el token verificado')
|
||||
if (req.usuarioToken.rol !== 'admin') {
|
||||
throw new AppError(403, 'Solo un administrador puede cambiar roles')
|
||||
}
|
||||
const { rol } = cambiarRolSchema.parse(req.body)
|
||||
if (!esRolValido(rol)) throw new BadRequestError(`Rol inválido: ${rol}`)
|
||||
|
||||
const usuario = await prisma.usuario.update({
|
||||
where: { id: req.params.id as string },
|
||||
data: { rol },
|
||||
})
|
||||
res.json(aPublico(usuario))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user