diff --git a/api-gateway/src/controllers/auditoria.controller.test.ts b/api-gateway/src/controllers/auditoria.controller.test.ts index bc0a3f1..cd6fa8f 100644 --- a/api-gateway/src/controllers/auditoria.controller.test.ts +++ b/api-gateway/src/controllers/auditoria.controller.test.ts @@ -8,6 +8,9 @@ const prismaMock = vi.hoisted(() => ({ auditoriaLog: { findMany: vi.fn(), }, + usuario: { + findMany: vi.fn(), + }, })) vi.mock('../services/prisma.js', () => ({ prisma: prismaMock })) @@ -29,13 +32,14 @@ beforeEach(() => { }) describe('listarAuditoriaEndpoint (GET /api/auditoria)', () => { - it('retorna logs de auditoría para admin', async () => { + it('retorna logs de auditoría para admin (con el nombre del usuario)', async () => { const logsFake = [{ id: 'log-1', usuarioId: 'u1', accion: 'CREAR_LEAD', entidad: 'lead', entidadId: 'l1', detalle: null, createdAt: new Date() }] prismaMock.auditoriaLog.findMany.mockResolvedValue(logsFake) + prismaMock.usuario.findMany.mockResolvedValue([{ id: 'u1', nombre: 'Martin Zavala' }]) const { res, json } = resFake() await listarAuditoriaEndpoint(reqFake({}, { sub: 'admin1', rol: 'admin' }), res) - expect(json).toHaveBeenCalledWith(logsFake) + expect(json).toHaveBeenCalledWith([{ ...logsFake[0], usuarioNombre: 'Martin Zavala' }]) }) it('aplica filtros de entidad y usuarioId', async () => { diff --git a/api-gateway/src/services/auditoria.service.test.ts b/api-gateway/src/services/auditoria.service.test.ts index 3eae19a..8abb601 100644 --- a/api-gateway/src/services/auditoria.service.test.ts +++ b/api-gateway/src/services/auditoria.service.test.ts @@ -7,6 +7,9 @@ const prismaMock = vi.hoisted(() => ({ create: vi.fn(), findMany: vi.fn(), }, + usuario: { + findMany: vi.fn(), + }, })) vi.mock('./prisma.js', () => ({ prisma: prismaMock })) @@ -48,9 +51,10 @@ describe('registrar', () => { }) describe('buscar', () => { - it('sin filtros: retorna todos los logs ordenados por createdAt desc', async () => { - const logsFake = [{ id: 'log-1' }] + it('sin filtros: retorna los logs con el nombre del usuario resuelto', async () => { + const logsFake = [{ id: 'log-1', usuarioId: 'u1' }] prismaMock.auditoriaLog.findMany.mockResolvedValue(logsFake) + prismaMock.usuario.findMany.mockResolvedValue([{ id: 'u1', nombre: 'Martin Zavala' }]) const resultado = await buscar({}) expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({ @@ -58,11 +62,23 @@ describe('buscar', () => { orderBy: { createdAt: 'desc' }, take: 100, }) - expect(resultado).toEqual(logsFake) + expect(resultado).toEqual([{ id: 'log-1', usuarioId: 'u1', usuarioNombre: 'Martin Zavala' }]) + }) + + it('si el usuario ya no existe usa el id; "sistema" se muestra como Sistema', async () => { + prismaMock.auditoriaLog.findMany.mockResolvedValue([ + { id: 'log-2', usuarioId: 'fantasma' }, + { id: 'log-3', usuarioId: 'sistema' }, + ]) + prismaMock.usuario.findMany.mockResolvedValue([]) + + const resultado = await buscar({}) + expect(resultado.map((r) => r.usuarioNombre)).toEqual(['fantasma', 'Sistema']) }) it('con filtros de entidad y usuarioId', async () => { prismaMock.auditoriaLog.findMany.mockResolvedValue([]) + prismaMock.usuario.findMany.mockResolvedValue([]) await buscar({ entidad: 'lead', usuarioId: 'u1' }) expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({ where: { entidad: 'lead', usuarioId: 'u1' }, @@ -75,6 +91,7 @@ describe('buscar', () => { const desde = new Date('2026-09-01') const hasta = new Date('2026-09-07') prismaMock.auditoriaLog.findMany.mockResolvedValue([]) + prismaMock.usuario.findMany.mockResolvedValue([]) await buscar({ desde, hasta }) expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({ where: { createdAt: { gte: desde, lte: hasta } }, @@ -85,6 +102,7 @@ describe('buscar', () => { it('respeta el límite personalizado', async () => { prismaMock.auditoriaLog.findMany.mockResolvedValue([]) + prismaMock.usuario.findMany.mockResolvedValue([]) await buscar({ entidad: 'lead' }, 10) expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({ where: { entidad: 'lead' }, diff --git a/api-gateway/src/services/auditoria.service.ts b/api-gateway/src/services/auditoria.service.ts index f22c623..966abbb 100644 --- a/api-gateway/src/services/auditoria.service.ts +++ b/api-gateway/src/services/auditoria.service.ts @@ -30,6 +30,11 @@ export function registrar( }) } +/** + * Lista registros de auditoría con el NOMBRE del usuario resuelto (la UI + * muestra nombres, no ids). Si el usuario ya no existe se devuelve el id como + * respaldo ('sistema' se muestra como 'Sistema'). + */ export async function buscar(filtros: FiltrosAuditoria, limite = 100) { const where: Record = {} if (filtros.entidad !== undefined) where['entidad'] = filtros.entidad @@ -41,9 +46,22 @@ export async function buscar(filtros: FiltrosAuditoria, limite = 100) { ...(filtros.hasta !== undefined ? { lte: filtros.hasta } : {}), } } - return prisma.auditoriaLog.findMany({ + const logs = await prisma.auditoriaLog.findMany({ where, orderBy: { createdAt: 'desc' }, take: limite, }) + + const ids = [...new Set(logs.map((l) => l.usuarioId))] + const usuarios = ids.length > 0 + ? await prisma.usuario.findMany({ where: { id: { in: ids } }, select: { id: true, nombre: true } }) + : [] + const nombrePorId = new Map(usuarios.map((u) => [u.id, u.nombre])) + + return logs.map((l) => ({ + ...l, + usuarioNombre: + nombrePorId.get(l.usuarioId) ?? + (l.usuarioId === 'sistema' || l.usuarioId === 'desconocido' ? 'Sistema' : l.usuarioId), + })) } diff --git a/apps/landing/src/components/config/AuditoriaLog.test.tsx b/apps/landing/src/components/config/AuditoriaLog.test.tsx index d2ded92..ed2ef12 100644 --- a/apps/landing/src/components/config/AuditoriaLog.test.tsx +++ b/apps/landing/src/components/config/AuditoriaLog.test.tsx @@ -23,8 +23,8 @@ const respuestaOk = (data: unknown) => new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } }) const auditoriaFake = [ - { id: 'a1', usuarioId: 'admin', accion: 'LEAD_CREADO', entidad: 'lead', entidadId: 'l1', detalle: 'Lead nuevo', createdAt: '2026-09-01T10:00:00Z' }, - { id: 'a2', usuarioId: 'vendedor1', accion: 'ETAPA_CAMBIADA', entidad: 'oportunidad', entidadId: 'o1', detalle: 'Cambiado a Cotizador', createdAt: '2026-09-02T14:00:00Z' }, + { id: 'a1', usuarioId: 'admin', usuarioNombre: 'Administrador FRASA', accion: 'LEAD_CREADO', entidad: 'lead', entidadId: 'l1', detalle: 'Lead nuevo', createdAt: '2026-09-01T10:00:00Z' }, + { id: 'a2', usuarioId: 'vendedor1', usuarioNombre: 'Martin Zavala', accion: 'ETAPA_CAMBIADA', entidad: 'oportunidad', entidadId: 'o1', detalle: 'Cambiado a Cotizador', createdAt: '2026-09-02T14:00:00Z' }, ] let fetchMock: ReturnType> @@ -49,9 +49,9 @@ describe('AuditoriaLog', () => { await waitFor(() => expect(fetchMock).toHaveBeenCalledOnce()) expect(screen.getByText('LEAD_CREADO')).toBeTruthy() expect(screen.getByText('ETAPA_CAMBIADA')).toBeTruthy() - // User IDs may appear in multiple cells; verify they exist - expect(screen.getAllByText('admin').length).toBeGreaterThanOrEqual(1) - expect(screen.getAllByText('vendedor1').length).toBeGreaterThanOrEqual(1) + // La tabla muestra el NOMBRE del usuario (el id solo como respaldo/tooltip) + expect(screen.getAllByText('Administrador FRASA').length).toBeGreaterThanOrEqual(1) + expect(screen.getAllByText('Martin Zavala').length).toBeGreaterThanOrEqual(1) }) it('muestra "Sin registros" cuando no hay datos', async () => { diff --git a/apps/landing/src/components/config/AuditoriaLog.tsx b/apps/landing/src/components/config/AuditoriaLog.tsx index 3ca572e..8fc44dd 100644 --- a/apps/landing/src/components/config/AuditoriaLog.tsx +++ b/apps/landing/src/components/config/AuditoriaLog.tsx @@ -15,7 +15,7 @@ export function AuditoriaLog() { const [usuario, setUsuario] = useState('') const [fechaDesde, setFechaDesde] = useState('') const [fechaHasta, setFechaHasta] = useState('') - const [usuariosLista, setUsuariosLista] = useState([]) + const [usuariosLista, setUsuariosLista] = useState<[string, string][]>([]) const cargar = useCallback(async () => { setCargando(true) @@ -29,9 +29,12 @@ export function AuditoriaLog() { }) setItems(data) setPagina(1) - // Extraer usuarios únicos para el select - const usrs = [...new Set(data.map((i) => i.usuarioId).filter(Boolean))] as string[] - setUsuariosLista(usrs) + // Usuarios únicos para el select: se muestra el nombre y se filtra por id + const porId = new Map() + for (const i of data) { + if (i.usuarioId) porId.set(i.usuarioId, i.usuarioNombre ?? i.usuarioId) + } + setUsuariosLista([...porId.entries()].sort((a, b) => a[1].localeCompare(b[1]))) } catch { setError('No se pudieron cargar los registros de auditoría.') } finally { @@ -74,9 +77,9 @@ export function AuditoriaLog() { className="ml-2 rounded-lg border border-[#D1D5DB] bg-white px-2 py-1.5 text-xs dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0]" > - {usuariosLista.map((u) => ( - ))} @@ -136,7 +139,9 @@ export function AuditoriaLog() { {new Date(item.createdAt).toLocaleString('es-MX')} - {item.usuarioId} + + {item.usuarioNombre ?? item.usuarioId} + {item.accion} {item.entidad} {item.detalle ?? '—'} diff --git a/apps/landing/src/services/config.ts b/apps/landing/src/services/config.ts index 13f16c7..81d4325 100644 --- a/apps/landing/src/services/config.ts +++ b/apps/landing/src/services/config.ts @@ -170,6 +170,8 @@ export async function reindexarConocimiento(): Promise<{ export interface AuditoriaItem { id: string usuarioId: string + /** Nombre del usuario que hizo la acción (el API lo resuelve) */ + usuarioNombre?: string accion: string entidad: string entidadId: string