feat(auditoria): mostrar el nombre del usuario en lugar del id (tabla y filtro)
This commit is contained in:
@@ -8,6 +8,9 @@ const prismaMock = vi.hoisted(() => ({
|
|||||||
auditoriaLog: {
|
auditoriaLog: {
|
||||||
findMany: vi.fn(),
|
findMany: vi.fn(),
|
||||||
},
|
},
|
||||||
|
usuario: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||||
@@ -29,13 +32,14 @@ beforeEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('listarAuditoriaEndpoint (GET /api/auditoria)', () => {
|
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() }]
|
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.auditoriaLog.findMany.mockResolvedValue(logsFake)
|
||||||
|
prismaMock.usuario.findMany.mockResolvedValue([{ id: 'u1', nombre: 'Martin Zavala' }])
|
||||||
const { res, json } = resFake()
|
const { res, json } = resFake()
|
||||||
|
|
||||||
await listarAuditoriaEndpoint(reqFake({}, { sub: 'admin1', rol: 'admin' }), res)
|
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 () => {
|
it('aplica filtros de entidad y usuarioId', async () => {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ const prismaMock = vi.hoisted(() => ({
|
|||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
findMany: vi.fn(),
|
findMany: vi.fn(),
|
||||||
},
|
},
|
||||||
|
usuario: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
|
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
|
||||||
@@ -48,9 +51,10 @@ describe('registrar', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('buscar', () => {
|
describe('buscar', () => {
|
||||||
it('sin filtros: retorna todos los logs ordenados por createdAt desc', async () => {
|
it('sin filtros: retorna los logs con el nombre del usuario resuelto', async () => {
|
||||||
const logsFake = [{ id: 'log-1' }]
|
const logsFake = [{ id: 'log-1', usuarioId: 'u1' }]
|
||||||
prismaMock.auditoriaLog.findMany.mockResolvedValue(logsFake)
|
prismaMock.auditoriaLog.findMany.mockResolvedValue(logsFake)
|
||||||
|
prismaMock.usuario.findMany.mockResolvedValue([{ id: 'u1', nombre: 'Martin Zavala' }])
|
||||||
|
|
||||||
const resultado = await buscar({})
|
const resultado = await buscar({})
|
||||||
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
||||||
@@ -58,11 +62,23 @@ describe('buscar', () => {
|
|||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 100,
|
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 () => {
|
it('con filtros de entidad y usuarioId', async () => {
|
||||||
prismaMock.auditoriaLog.findMany.mockResolvedValue([])
|
prismaMock.auditoriaLog.findMany.mockResolvedValue([])
|
||||||
|
prismaMock.usuario.findMany.mockResolvedValue([])
|
||||||
await buscar({ entidad: 'lead', usuarioId: 'u1' })
|
await buscar({ entidad: 'lead', usuarioId: 'u1' })
|
||||||
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
||||||
where: { entidad: 'lead', usuarioId: 'u1' },
|
where: { entidad: 'lead', usuarioId: 'u1' },
|
||||||
@@ -75,6 +91,7 @@ describe('buscar', () => {
|
|||||||
const desde = new Date('2026-09-01')
|
const desde = new Date('2026-09-01')
|
||||||
const hasta = new Date('2026-09-07')
|
const hasta = new Date('2026-09-07')
|
||||||
prismaMock.auditoriaLog.findMany.mockResolvedValue([])
|
prismaMock.auditoriaLog.findMany.mockResolvedValue([])
|
||||||
|
prismaMock.usuario.findMany.mockResolvedValue([])
|
||||||
await buscar({ desde, hasta })
|
await buscar({ desde, hasta })
|
||||||
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
||||||
where: { createdAt: { gte: desde, lte: hasta } },
|
where: { createdAt: { gte: desde, lte: hasta } },
|
||||||
@@ -85,6 +102,7 @@ describe('buscar', () => {
|
|||||||
|
|
||||||
it('respeta el límite personalizado', async () => {
|
it('respeta el límite personalizado', async () => {
|
||||||
prismaMock.auditoriaLog.findMany.mockResolvedValue([])
|
prismaMock.auditoriaLog.findMany.mockResolvedValue([])
|
||||||
|
prismaMock.usuario.findMany.mockResolvedValue([])
|
||||||
await buscar({ entidad: 'lead' }, 10)
|
await buscar({ entidad: 'lead' }, 10)
|
||||||
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
||||||
where: { entidad: 'lead' },
|
where: { entidad: 'lead' },
|
||||||
|
|||||||
@@ -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) {
|
export async function buscar(filtros: FiltrosAuditoria, limite = 100) {
|
||||||
const where: Record<string, unknown> = {}
|
const where: Record<string, unknown> = {}
|
||||||
if (filtros.entidad !== undefined) where['entidad'] = filtros.entidad
|
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 } : {}),
|
...(filtros.hasta !== undefined ? { lte: filtros.hasta } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prisma.auditoriaLog.findMany({
|
const logs = await prisma.auditoriaLog.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: limite,
|
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),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const respuestaOk = (data: unknown) =>
|
|||||||
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
||||||
|
|
||||||
const auditoriaFake = [
|
const auditoriaFake = [
|
||||||
{ id: 'a1', usuarioId: 'admin', accion: 'LEAD_CREADO', entidad: 'lead', entidadId: 'l1', detalle: 'Lead nuevo', createdAt: '2026-09-01T10: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', accion: 'ETAPA_CAMBIADA', entidad: 'oportunidad', entidadId: 'o1', detalle: 'Cambiado a Cotizador', createdAt: '2026-09-02T14: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<typeof vi.fn<FetchFn>>
|
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
|
||||||
@@ -49,9 +49,9 @@ describe('AuditoriaLog', () => {
|
|||||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledOnce())
|
await waitFor(() => expect(fetchMock).toHaveBeenCalledOnce())
|
||||||
expect(screen.getByText('LEAD_CREADO')).toBeTruthy()
|
expect(screen.getByText('LEAD_CREADO')).toBeTruthy()
|
||||||
expect(screen.getByText('ETAPA_CAMBIADA')).toBeTruthy()
|
expect(screen.getByText('ETAPA_CAMBIADA')).toBeTruthy()
|
||||||
// User IDs may appear in multiple cells; verify they exist
|
// La tabla muestra el NOMBRE del usuario (el id solo como respaldo/tooltip)
|
||||||
expect(screen.getAllByText('admin').length).toBeGreaterThanOrEqual(1)
|
expect(screen.getAllByText('Administrador FRASA').length).toBeGreaterThanOrEqual(1)
|
||||||
expect(screen.getAllByText('vendedor1').length).toBeGreaterThanOrEqual(1)
|
expect(screen.getAllByText('Martin Zavala').length).toBeGreaterThanOrEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('muestra "Sin registros" cuando no hay datos', async () => {
|
it('muestra "Sin registros" cuando no hay datos', async () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function AuditoriaLog() {
|
|||||||
const [usuario, setUsuario] = useState('')
|
const [usuario, setUsuario] = useState('')
|
||||||
const [fechaDesde, setFechaDesde] = useState('')
|
const [fechaDesde, setFechaDesde] = useState('')
|
||||||
const [fechaHasta, setFechaHasta] = useState('')
|
const [fechaHasta, setFechaHasta] = useState('')
|
||||||
const [usuariosLista, setUsuariosLista] = useState<string[]>([])
|
const [usuariosLista, setUsuariosLista] = useState<[string, string][]>([])
|
||||||
|
|
||||||
const cargar = useCallback(async () => {
|
const cargar = useCallback(async () => {
|
||||||
setCargando(true)
|
setCargando(true)
|
||||||
@@ -29,9 +29,12 @@ export function AuditoriaLog() {
|
|||||||
})
|
})
|
||||||
setItems(data)
|
setItems(data)
|
||||||
setPagina(1)
|
setPagina(1)
|
||||||
// Extraer usuarios únicos para el select
|
// Usuarios únicos para el select: se muestra el nombre y se filtra por id
|
||||||
const usrs = [...new Set(data.map((i) => i.usuarioId).filter(Boolean))] as string[]
|
const porId = new Map<string, string>()
|
||||||
setUsuariosLista(usrs)
|
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 {
|
} catch {
|
||||||
setError('No se pudieron cargar los registros de auditoría.')
|
setError('No se pudieron cargar los registros de auditoría.')
|
||||||
} finally {
|
} 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]"
|
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]"
|
||||||
>
|
>
|
||||||
<option value="">Todos</option>
|
<option value="">Todos</option>
|
||||||
{usuariosLista.map((u) => (
|
{usuariosLista.map(([id, nombre]) => (
|
||||||
<option key={u} value={u}>
|
<option key={id} value={id}>
|
||||||
{u}
|
{nombre}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -136,7 +139,9 @@ export function AuditoriaLog() {
|
|||||||
<td className="whitespace-nowrap px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">
|
<td className="whitespace-nowrap px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">
|
||||||
{new Date(item.createdAt).toLocaleString('es-MX')}
|
{new Date(item.createdAt).toLocaleString('es-MX')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">{item.usuarioId}</td>
|
<td className="whitespace-nowrap px-3 py-2 text-[#374151] dark:text-[#CBD5E1]" title={item.usuarioId}>
|
||||||
|
{item.usuarioNombre ?? item.usuarioId}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">{item.accion}</td>
|
<td className="px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">{item.accion}</td>
|
||||||
<td className="px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">{item.entidad}</td>
|
<td className="px-3 py-2 text-[#374151] dark:text-[#CBD5E1]">{item.entidad}</td>
|
||||||
<td className="max-w-[200px] truncate px-3 py-2 text-[#64748B] dark:text-[#94A3B8]">{item.detalle ?? '—'}</td>
|
<td className="max-w-[200px] truncate px-3 py-2 text-[#64748B] dark:text-[#94A3B8]">{item.detalle ?? '—'}</td>
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ export async function reindexarConocimiento(): Promise<{
|
|||||||
export interface AuditoriaItem {
|
export interface AuditoriaItem {
|
||||||
id: string
|
id: string
|
||||||
usuarioId: string
|
usuarioId: string
|
||||||
|
/** Nombre del usuario que hizo la acción (el API lo resuelve) */
|
||||||
|
usuarioNombre?: string
|
||||||
accion: string
|
accion: string
|
||||||
entidad: string
|
entidad: string
|
||||||
entidadId: string
|
entidadId: string
|
||||||
|
|||||||
Reference in New Issue
Block a user