Mejora UI de página de auditoría
- Por defecto muestra solo logs del día actual - Agregado selector de período rápido (Hoy, Ayer, 7 días, 30 días, Personalizado) - Filtros avanzados colapsables para mejor UX - Interfaz más limpia y organizada - Formato de fechas mejorado (hora corta para hoy) - Estadísticas visuales mejoradas - Tabla simplificada con información esencial - Modal de detalles mantiene toda la información completa
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { api } from '$lib/utils/api';
|
import { api } from '$lib/utils/api';
|
||||||
import { toast } from '$lib/stores/toast';
|
import { toast } from '$lib/stores/toast';
|
||||||
@@ -12,40 +12,98 @@
|
|||||||
let selectedLog = null;
|
let selectedLog = null;
|
||||||
let showDetailModal = false;
|
let showDetailModal = false;
|
||||||
|
|
||||||
// Paginaci├│n
|
// Paginación
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
let totalLogs = 0;
|
let totalLogs = 0;
|
||||||
const perPage = 20;
|
const perPage = 20;
|
||||||
|
|
||||||
// Filtros
|
// Filtros básicos
|
||||||
let filterUserId = '';
|
let filterUserId = '';
|
||||||
let filterAction = '';
|
let filterAction = '';
|
||||||
let filterResourceType = '';
|
let filterResourceType = '';
|
||||||
let filterDateFrom = '';
|
|
||||||
let filterDateTo = '';
|
|
||||||
let searchText = '';
|
let searchText = '';
|
||||||
|
|
||||||
// Contador de filtros activos
|
// Filtro de período
|
||||||
$: activeFiltersCount = [filterUserId, filterAction, filterResourceType, filterDateFrom, filterDateTo, searchText].filter(f => f && f.trim()).length;
|
let periodFilter: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'custom' = 'today';
|
||||||
|
let customDateFrom = '';
|
||||||
|
let customDateTo = '';
|
||||||
|
|
||||||
// Tipos de acciones y recursos (extraídos de los logs)
|
// Control de visibilidad de filtros avanzados
|
||||||
|
let showAdvancedFilters = false;
|
||||||
|
|
||||||
|
// Contador de filtros activos (excluyendo el período que es por defecto)
|
||||||
|
$: activeFiltersCount = [filterUserId, filterAction, filterResourceType, searchText].filter(f => f && f.trim()).length;
|
||||||
|
|
||||||
|
// Tipos de acciones y recursos (extraídos de los logs)
|
||||||
let availableActions = new Set<string>();
|
let availableActions = new Set<string>();
|
||||||
let availableResourceTypes = new Set<string>();
|
let availableResourceTypes = new Set<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cargar estadísticas de auditoría
|
* Obtener fechas según el período seleccionado
|
||||||
|
*/
|
||||||
|
function getDateRangeForPeriod(): { from: string; to: string } {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
let from: Date;
|
||||||
|
let to: Date = new Date();
|
||||||
|
|
||||||
|
switch (periodFilter) {
|
||||||
|
case 'today':
|
||||||
|
from = new Date(today);
|
||||||
|
break;
|
||||||
|
case 'yesterday':
|
||||||
|
from = new Date(today);
|
||||||
|
from.setDate(from.getDate() - 1);
|
||||||
|
to = new Date(today);
|
||||||
|
to.setSeconds(to.getSeconds() - 1);
|
||||||
|
break;
|
||||||
|
case 'last7days':
|
||||||
|
from = new Date(today);
|
||||||
|
from.setDate(from.getDate() - 7);
|
||||||
|
break;
|
||||||
|
case 'last30days':
|
||||||
|
from = new Date(today);
|
||||||
|
from.setDate(from.getDate() - 30);
|
||||||
|
break;
|
||||||
|
case 'custom':
|
||||||
|
return {
|
||||||
|
from: customDateFrom,
|
||||||
|
to: customDateTo
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
from = new Date(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: from.toISOString().split('T')[0],
|
||||||
|
to: to.toISOString().split('T')[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cambiar período y actualizar datos
|
||||||
|
*/
|
||||||
|
function changePeriod(period: typeof periodFilter) {
|
||||||
|
periodFilter = period;
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cargar estadísticas de auditoría
|
||||||
*/
|
*/
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
stats = await api.get('/audit/stats');
|
stats = await api.get('/audit/stats');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error cargando estadísticas:', e);
|
console.error('Error cargando estadísticas:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cargar logs de auditoría con filtros
|
* Cargar logs de auditoría con filtros
|
||||||
*/
|
*/
|
||||||
async function loadLogs() {
|
async function loadLogs() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -55,11 +113,15 @@
|
|||||||
per_page: perPage
|
per_page: perPage
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aplicar rango de fechas según período
|
||||||
|
const dateRange = getDateRangeForPeriod();
|
||||||
|
if (dateRange.from) params.date_from = dateRange.from;
|
||||||
|
if (dateRange.to) params.date_to = dateRange.to;
|
||||||
|
|
||||||
|
// Aplicar filtros adicionales
|
||||||
if (filterUserId) params.user_id = filterUserId;
|
if (filterUserId) params.user_id = filterUserId;
|
||||||
if (filterAction) params.action = filterAction;
|
if (filterAction) params.action = filterAction;
|
||||||
if (filterResourceType) params.resource_type = filterResourceType;
|
if (filterResourceType) params.resource_type = filterResourceType;
|
||||||
if (filterDateFrom) params.date_from = filterDateFrom;
|
|
||||||
if (filterDateTo) params.date_to = filterDateTo;
|
|
||||||
if (searchText) params.search = searchText;
|
if (searchText) params.search = searchText;
|
||||||
|
|
||||||
const response = await api.get('/audit/', params);
|
const response = await api.get('/audit/', params);
|
||||||
@@ -69,7 +131,7 @@
|
|||||||
totalPages = response.total_pages;
|
totalPages = response.total_pages;
|
||||||
currentPage = response.page;
|
currentPage = response.page;
|
||||||
|
|
||||||
// Extraer acciones y tipos de recursos ├║nicos para los selectores
|
// Extraer acciones y tipos de recursos únicos para los selectores
|
||||||
logs.forEach((log: any) => {
|
logs.forEach((log: any) => {
|
||||||
availableActions.add(log.action);
|
availableActions.add(log.action);
|
||||||
availableResourceTypes.add(log.resource_type);
|
availableResourceTypes.add(log.resource_type);
|
||||||
@@ -98,7 +160,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aplicar filtros y recargar desde página 1
|
* Aplicar filtros y recargar desde página 1
|
||||||
*/
|
*/
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
@@ -106,21 +168,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpiar todos los filtros
|
* Limpiar todos los filtros (excepto el período)
|
||||||
*/
|
*/
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
filterUserId = '';
|
filterUserId = '';
|
||||||
filterAction = '';
|
filterAction = '';
|
||||||
filterResourceType = '';
|
filterResourceType = '';
|
||||||
filterDateFrom = '';
|
|
||||||
filterDateTo = '';
|
|
||||||
searchText = '';
|
searchText = '';
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadLogs();
|
loadLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cambiar página
|
* Cambiar página
|
||||||
*/
|
*/
|
||||||
function goToPage(page: number) {
|
function goToPage(page: number) {
|
||||||
if (page >= 1 && page <= totalPages) {
|
if (page >= 1 && page <= totalPages) {
|
||||||
@@ -152,7 +212,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener color de badge seg├║n tipo de acci├│n
|
* Formatear fecha corta (solo hora para hoy)
|
||||||
|
*/
|
||||||
|
function formatDateShort(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const today = new Date();
|
||||||
|
const isToday = date.toDateString() === today.toDateString();
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
return date.toLocaleTimeString('es-MX', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString('es-MX', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener color de badge según tipo de acción
|
||||||
*/
|
*/
|
||||||
function getActionColor(action: string): string {
|
function getActionColor(action: string): string {
|
||||||
if (action.includes('login')) return 'bg-green-100 text-green-800';
|
if (action.includes('login')) return 'bg-green-100 text-green-800';
|
||||||
@@ -165,44 +248,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatear acci├│n con informaci├│n del rol del usuario
|
* Formatear acción de forma legible
|
||||||
*/
|
*/
|
||||||
function formatActionWithRole(log: any): string {
|
function formatActionText(action: string): string {
|
||||||
const actionParts = log.action.split('.');
|
const parts = action.split('.');
|
||||||
if (actionParts.length !== 2) return log.action;
|
if (parts.length !== 2) return action;
|
||||||
|
|
||||||
const [resource, verb] = actionParts;
|
const [resource, verb] = parts;
|
||||||
|
|
||||||
const verbMap: Record<string, string> = {
|
const verbMap: Record<string, string> = {
|
||||||
'login': 'inici├│ sesi├│n',
|
'login': 'Inicio de sesión',
|
||||||
'logout': 'cerr├│ sesi├│n',
|
'logout': 'Cierre de sesión',
|
||||||
'create': 'cre├│',
|
'create': 'Creó',
|
||||||
'update': 'actualiz├│',
|
'update': 'Actualizó',
|
||||||
'delete': 'elimin├│',
|
'delete': 'Eliminó',
|
||||||
'assign': 'asign├│',
|
'assign': 'Asignó',
|
||||||
'close': 'cerr├│',
|
'close': 'Cerró',
|
||||||
'reopen': 'reabri├│'
|
'reopen': 'Reabrió'
|
||||||
};
|
|
||||||
|
|
||||||
const roleMap: Record<string, string> = {
|
|
||||||
'ADMIN': 'Administrador',
|
|
||||||
'SUPPORT_MANAGER': 'Gerente de Soporte',
|
|
||||||
'AGENT': 'Agente',
|
|
||||||
'AUDITOR': 'Auditor',
|
|
||||||
'CLIENT_ADMIN': 'Admin de Cliente',
|
|
||||||
'CLIENT_USER': 'Usuario de Cliente'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const verbText = verbMap[verb] || verb;
|
const verbText = verbMap[verb] || verb;
|
||||||
const resourceText = resource;
|
return `${verbText} ${resource}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Si hay rol de usuario, incluirlo
|
/**
|
||||||
if (log.user_role) {
|
* Obtener texto del rol
|
||||||
const roleText = roleMap[log.user_role] || log.user_role;
|
*/
|
||||||
return `${verbText} ${resourceText} (${roleText})`;
|
function getRoleText(role: string): string {
|
||||||
}
|
const roleMap: Record<string, string> = {
|
||||||
|
'ADMIN': 'Administrador',
|
||||||
return `${verbText} ${resourceText}`;
|
'SUPPORT_MANAGER': 'Gerente',
|
||||||
|
'AGENT': 'Agente',
|
||||||
|
'AUDITOR': 'Auditor',
|
||||||
|
'CLIENT_ADMIN': 'Admin Cliente',
|
||||||
|
'CLIENT_USER': 'Usuario'
|
||||||
|
};
|
||||||
|
return roleMap[role] || role;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,207 +298,225 @@
|
|||||||
|
|
||||||
<div class="px-4 sm:px-6 lg:px-8 py-8">
|
<div class="px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="sm:flex sm:items-center sm:justify-between">
|
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold text-gray-900">Auditoría</h1>
|
<h1 class="text-2xl font-semibold text-gray-900">Auditoría del Sistema</h1>
|
||||||
<p class="mt-2 text-sm text-gray-700">
|
<p class="mt-1 text-sm text-gray-600">
|
||||||
Registro completo de todas las acciones realizadas en el sistema
|
Registro de actividades •
|
||||||
|
<span class="font-medium text-primary-600">
|
||||||
|
{periodFilter === 'today' ? 'Hoy' :
|
||||||
|
periodFilter === 'yesterday' ? 'Ayer' :
|
||||||
|
periodFilter === 'last7days' ? 'Últimos 7 días' :
|
||||||
|
periodFilter === 'last30days' ? 'Últimos 30 días' :
|
||||||
|
'Período personalizado'}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Estadísticas -->
|
<!-- Selector de Período -->
|
||||||
|
<div class="bg-white shadow rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
on:click={() => changePeriod('today')}
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'today' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
|
>
|
||||||
|
Hoy
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={() => changePeriod('yesterday')}
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'yesterday' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
|
>
|
||||||
|
Ayer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={() => changePeriod('last7days')}
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'last7days' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
|
>
|
||||||
|
Últimos 7 días
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={() => changePeriod('last30days')}
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'last30days' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
|
>
|
||||||
|
Últimos 30 días
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={() => changePeriod('custom')}
|
||||||
|
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'custom' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Personalizado
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rango de fechas personalizado -->
|
||||||
|
{#if periodFilter === 'custom'}
|
||||||
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
|
||||||
|
<div>
|
||||||
|
<label for="custom-date-from" class="block text-sm font-medium text-gray-700 mb-1">Desde</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="custom-date-from"
|
||||||
|
bind:value={customDateFrom}
|
||||||
|
on:change={applyFilters}
|
||||||
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="custom-date-to" class="block text-sm font-medium text-gray-700 mb-1">Hasta</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="custom-date-to"
|
||||||
|
bind:value={customDateTo}
|
||||||
|
on:change={applyFilters}
|
||||||
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estadísticas Rápidas -->
|
||||||
{#if stats}
|
{#if stats}
|
||||||
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
<div class="p-5">
|
<div class="text-sm text-gray-500">Total</div>
|
||||||
<div class="flex items-center">
|
<div class="text-2xl font-bold text-gray-900">{stats.total_actions.toLocaleString()}</div>
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-5 w-0 flex-1">
|
|
||||||
<dl>
|
|
||||||
<dt class="text-sm font-medium text-gray-500 truncate">Total de Acciones</dt>
|
|
||||||
<dd class="text-lg font-semibold text-gray-900">{stats.total_actions.toLocaleString()}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
<div class="text-sm text-gray-500">Hoy</div>
|
||||||
<div class="p-5">
|
<div class="text-2xl font-bold text-primary-600">{stats.actions_today}</div>
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-6 w-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-5 w-0 flex-1">
|
|
||||||
<dl>
|
|
||||||
<dt class="text-sm font-medium text-gray-500 truncate">Hoy</dt>
|
|
||||||
<dd class="text-lg font-semibold text-gray-900">{stats.actions_today}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
<div class="text-sm text-gray-500">Esta Semana</div>
|
||||||
<div class="p-5">
|
<div class="text-2xl font-bold text-green-600">{stats.actions_this_week}</div>
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-6 w-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-5 w-0 flex-1">
|
|
||||||
<dl>
|
|
||||||
<dt class="text-sm font-medium text-gray-500 truncate">Esta Semana</dt>
|
|
||||||
<dd class="text-lg font-semibold text-gray-900">{stats.actions_this_week}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
<div class="text-sm text-gray-500 truncate">Más Común</div>
|
||||||
<div class="p-5">
|
<div class="text-sm font-semibold text-gray-900 truncate">
|
||||||
<div class="flex items-center">
|
{#if stats.top_actions && Object.keys(stats.top_actions).length > 0}
|
||||||
<div class="flex-shrink-0">
|
{formatActionText(Object.keys(stats.top_actions)[0])}
|
||||||
<svg class="h-6 w-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{:else}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
N/A
|
||||||
</svg>
|
{/if}
|
||||||
</div>
|
|
||||||
<div class="ml-5 w-0 flex-1">
|
|
||||||
<dl>
|
|
||||||
<dt class="text-sm font-medium text-gray-500 truncate">Acción más Común</dt>
|
|
||||||
<dd class="text-sm font-semibold text-gray-900 truncate">
|
|
||||||
{#if stats.top_actions && Object.keys(stats.top_actions).length > 0}
|
|
||||||
{Object.keys(stats.top_actions)[0]}
|
|
||||||
{:else}
|
|
||||||
N/A
|
|
||||||
{/if}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros Avanzados (Colapsables) -->
|
||||||
<div class="mt-6 bg-white shadow rounded-lg p-6">
|
<div class="bg-white shadow rounded-lg mb-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<button
|
||||||
<h3 class="text-lg font-medium text-gray-900">Filtros</h3>
|
on:click={() => showAdvancedFilters = !showAdvancedFilters}
|
||||||
{#if activeFiltersCount > 0}
|
class="w-full px-4 py-3 flex items-center justify-between text-left hover:bg-gray-50 rounded-lg transition-colors"
|
||||||
<button
|
>
|
||||||
on:click={clearFilters}
|
<div class="flex items-center gap-2">
|
||||||
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||||
Limpiar ({activeFiltersCount})
|
</svg>
|
||||||
</button>
|
<span class="text-sm font-medium text-gray-900">Filtros Avanzados</span>
|
||||||
{/if}
|
{#if activeFiltersCount > 0}
|
||||||
</div>
|
<span class="px-2 py-0.5 rounded-full bg-primary-100 text-primary-700 text-xs font-medium">
|
||||||
|
{activeFiltersCount}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
</span>
|
||||||
<!-- B├║squeda de texto -->
|
{/if}
|
||||||
<div>
|
|
||||||
<label for="search" class="block text-sm font-medium text-gray-700">Buscar</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="search"
|
|
||||||
bind:value={searchText}
|
|
||||||
on:input={applyFilters}
|
|
||||||
placeholder="Buscar en acciones..."
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<svg class="w-5 h-5 text-gray-400 transition-transform {showAdvancedFilters ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Filtro por usuario -->
|
{#if showAdvancedFilters}
|
||||||
<div>
|
<div class="px-4 pb-4 border-t">
|
||||||
<label for="user" class="block text-sm font-medium text-gray-700">Usuario</label>
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||||
<select
|
<!-- Búsqueda de texto -->
|
||||||
id="user"
|
<div>
|
||||||
bind:value={filterUserId}
|
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Buscar</label>
|
||||||
on:change={applyFilters}
|
<input
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
type="text"
|
||||||
>
|
id="search"
|
||||||
<option value="">Todos los usuarios</option>
|
bind:value={searchText}
|
||||||
{#each users as user}
|
on:input={applyFilters}
|
||||||
<option value={user.id}>{user.first_name} {user.last_name} ({user.email})</option>
|
placeholder="Buscar en acciones..."
|
||||||
{/each}
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtro por acci├│n -->
|
<!-- Filtro por usuario -->
|
||||||
<div>
|
<div>
|
||||||
<label for="action" class="block text-sm font-medium text-gray-700">Acci├│n</label>
|
<label for="user" class="block text-sm font-medium text-gray-700 mb-1">Usuario</label>
|
||||||
<select
|
<select
|
||||||
id="action"
|
id="user"
|
||||||
bind:value={filterAction}
|
bind:value={filterUserId}
|
||||||
on:change={applyFilters}
|
on:change={applyFilters}
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Todas las acciones</option>
|
<option value="">Todos</option>
|
||||||
{#each Array.from(availableActions).sort() as action}
|
{#each users as user}
|
||||||
<option value={action}>{action}</option>
|
<option value={user.id}>{user.first_name} {user.last_name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtro por tipo de recurso -->
|
<!-- Filtro por acción -->
|
||||||
<div>
|
<div>
|
||||||
<label for="resource-type" class="block text-sm font-medium text-gray-700">Tipo de Recurso</label>
|
<label for="action" class="block text-sm font-medium text-gray-700 mb-1">Acción</label>
|
||||||
<select
|
<select
|
||||||
id="resource-type"
|
id="action"
|
||||||
bind:value={filterResourceType}
|
bind:value={filterAction}
|
||||||
on:change={applyFilters}
|
on:change={applyFilters}
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Todos los tipos</option>
|
<option value="">Todas</option>
|
||||||
{#each Array.from(availableResourceTypes).sort() as resourceType}
|
{#each Array.from(availableActions).sort() as action}
|
||||||
<option value={resourceType}>{resourceType}</option>
|
<option value={action}>{formatActionText(action)}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fecha desde -->
|
<!-- Filtro por tipo de recurso -->
|
||||||
<div>
|
<div>
|
||||||
<label for="date-from" class="block text-sm font-medium text-gray-700">Desde</label>
|
<label for="resource-type" class="block text-sm font-medium text-gray-700 mb-1">Tipo de Recurso</label>
|
||||||
<input
|
<select
|
||||||
type="date"
|
id="resource-type"
|
||||||
id="date-from"
|
bind:value={filterResourceType}
|
||||||
bind:value={filterDateFrom}
|
on:change={applyFilters}
|
||||||
on:change={applyFilters}
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
>
|
||||||
/>
|
<option value="">Todos</option>
|
||||||
</div>
|
{#each Array.from(availableResourceTypes).sort() as resourceType}
|
||||||
|
<option value={resourceType}>{resourceType}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Fecha hasta -->
|
{#if activeFiltersCount > 0}
|
||||||
<div>
|
<div class="mt-4 flex justify-end">
|
||||||
<label for="date-to" class="block text-sm font-medium text-gray-700">Hasta</label>
|
<button
|
||||||
<input
|
on:click={clearFilters}
|
||||||
type="date"
|
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||||||
id="date-to"
|
>
|
||||||
bind:value={filterDateTo}
|
Limpiar filtros
|
||||||
on:change={applyFilters}
|
</button>
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
|
</div>
|
||||||
/>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabla de Logs -->
|
<!-- Tabla de Logs -->
|
||||||
<div class="mt-6 bg-white shadow rounded-lg overflow-hidden">
|
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||||
<div class="px-6 py-4 border-b border-gray-200">
|
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||||
<h3 class="text-lg font-medium text-gray-900">
|
<div class="flex items-center justify-between">
|
||||||
Logs de Auditoría
|
<h3 class="text-sm font-medium text-gray-900">
|
||||||
<span class="text-sm text-gray-500 font-normal">({totalLogs} registros)</span>
|
Registros de Auditoría
|
||||||
</h3>
|
</h3>
|
||||||
|
<span class="text-sm text-gray-500">{totalLogs} registros</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
@@ -427,85 +526,68 @@
|
|||||||
{:else if logs.length === 0}
|
{:else if logs.length === 0}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No hay logs</h3>
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No hay registros</h3>
|
||||||
<p class="mt-1 text-sm text-gray-500">No se encontraron registros con los filtros aplicados.</p>
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{periodFilter === 'today' ? 'No hay actividad registrada hoy.' : 'No se encontraron registros para el período seleccionado.'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Fecha/Hora
|
Hora
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Usuario
|
Usuario
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Acci├│n
|
Acción
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Recurso
|
Recurso
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th scope="col" class="relative px-4 py-3">
|
||||||
IP
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="relative px-6 py-3">
|
|
||||||
<span class="sr-only">Acciones</span>
|
<span class="sr-only">Acciones</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
{#each logs as log (log.id)}
|
{#each logs as log (log.id)}
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50 transition-colors">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||||
{formatDate(log.created_at)}
|
{formatDateShort(log.created_at)}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
||||||
{#if log.user_email}
|
{#if log.user_email}
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900">{log.user_name || 'N/A'}</div>
|
<div class="font-medium text-gray-900">{log.user_name || 'N/A'}</div>
|
||||||
<div class="text-gray-500">{log.user_email}</div>
|
<div class="text-xs text-gray-500">{getRoleText(log.user_role)}</div>
|
||||||
{#if log.user_role}
|
|
||||||
<div class="text-xs text-gray-400 mt-1">
|
|
||||||
{log.user_role === 'ADMIN' ? 'Administrador' :
|
|
||||||
log.user_role === 'SUPPORT_MANAGER' ? 'Gerente de Soporte' :
|
|
||||||
log.user_role === 'AGENT' ? 'Agente' :
|
|
||||||
log.user_role === 'AUDITOR' ? 'Auditor' :
|
|
||||||
log.user_role === 'CLIENT_ADMIN' ? 'Admin de Cliente' :
|
|
||||||
log.user_role === 'CLIENT_USER' ? 'Usuario de Cliente' :
|
|
||||||
log.user_role}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-gray-500 italic">Sistema</span>
|
<span class="text-gray-500 italic">Sistema</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {getActionColor(log.action)}">
|
<span class="px-2 py-1 text-xs font-medium rounded-full {getActionColor(log.action)}">
|
||||||
{formatActionWithRole(log)}
|
{formatActionText(log.action)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
||||||
<div>
|
<div class="text-gray-900 font-medium">{log.resource_type}</div>
|
||||||
<div class="font-medium">{log.resource_type}</div>
|
{#if log.ip_address}
|
||||||
{#if log.resource_id}
|
<div class="text-xs text-gray-500">{log.ip_address}</div>
|
||||||
<div class="text-gray-500 text-xs truncate max-w-xs">{log.resource_id}</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td class="px-4 py-3 whitespace-nowrap text-right text-sm">
|
||||||
{log.ip_address || 'N/A'}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<button
|
<button
|
||||||
on:click={() => viewDetail(log)}
|
on:click={() => viewDetail(log)}
|
||||||
class="text-primary-600 hover:text-primary-900"
|
class="text-primary-600 hover:text-primary-900 font-medium"
|
||||||
>
|
>
|
||||||
Ver detalle
|
Detalle
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -514,9 +596,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Paginaci├│n -->
|
<!-- Paginación -->
|
||||||
{#if totalPages > 1}
|
{#if totalPages > 1}
|
||||||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200">
|
||||||
<div class="flex-1 flex justify-between sm:hidden">
|
<div class="flex-1 flex justify-between sm:hidden">
|
||||||
<button
|
<button
|
||||||
on:click={() => goToPage(currentPage - 1)}
|
on:click={() => goToPage(currentPage - 1)}
|
||||||
@@ -588,18 +670,14 @@
|
|||||||
|
|
||||||
<!-- Modal de Detalle -->
|
<!-- Modal de Detalle -->
|
||||||
{#if showDetailModal && selectedLog}
|
{#if showDetailModal && selectedLog}
|
||||||
<Modal title="Detalle del Log de Auditoría" on:close={() => showDetailModal = false}>
|
<Modal title="Detalle del Registro de Auditoría" on:close={() => showDetailModal = false}>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Informaci├│n General -->
|
<!-- Información General -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-gray-900 mb-2">Informaci├│n General</h4>
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Información General</h4>
|
||||||
<dl class="grid grid-cols-2 gap-3 text-sm">
|
<dl class="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<dt class="font-medium text-gray-500">ID:</dt>
|
<dt class="font-medium text-gray-500">Fecha y Hora:</dt>
|
||||||
<dd class="text-gray-900 font-mono text-xs">{selectedLog.id}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt class="font-medium text-gray-500">Fecha:</dt>
|
|
||||||
<dd class="text-gray-900">{formatDate(selectedLog.created_at)}</dd>
|
<dd class="text-gray-900">{formatDate(selectedLog.created_at)}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -613,36 +691,28 @@
|
|||||||
{#if selectedLog.user_role}
|
{#if selectedLog.user_role}
|
||||||
<div>
|
<div>
|
||||||
<dt class="font-medium text-gray-500">Rol:</dt>
|
<dt class="font-medium text-gray-500">Rol:</dt>
|
||||||
<dd class="text-gray-900">
|
<dd class="text-gray-900">{getRoleText(selectedLog.user_role)}</dd>
|
||||||
{selectedLog.user_role === 'ADMIN' ? 'Administrador' :
|
|
||||||
selectedLog.user_role === 'SUPPORT_MANAGER' ? 'Gerente de Soporte' :
|
|
||||||
selectedLog.user_role === 'AGENT' ? 'Agente' :
|
|
||||||
selectedLog.user_role === 'AUDITOR' ? 'Auditor' :
|
|
||||||
selectedLog.user_role === 'CLIENT_ADMIN' ? 'Admin de Cliente' :
|
|
||||||
selectedLog.user_role === 'CLIENT_USER' ? 'Usuario de Cliente' :
|
|
||||||
selectedLog.user_role}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
<dt class="font-medium text-gray-500">IP:</dt>
|
<dt class="font-medium text-gray-500">IP:</dt>
|
||||||
<dd class="text-gray-900">{selectedLog.ip_address || 'N/A'}</dd>
|
<dd class="text-gray-900 font-mono text-xs">{selectedLog.ip_address || 'N/A'}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt class="font-medium text-gray-500">Correlation ID:</dt>
|
<dt class="font-medium text-gray-500">Correlation ID:</dt>
|
||||||
<dd class="text-gray-900 font-mono text-xs">{selectedLog.correlation_id || 'N/A'}</dd>
|
<dd class="text-gray-900 font-mono text-xs truncate">{selectedLog.correlation_id || 'N/A'}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Acci├│n -->
|
<!-- Acción -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-gray-900 mb-2">Acci├│n</h4>
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Acción</h4>
|
||||||
<div class="bg-gray-50 rounded-lg p-3">
|
<div class="bg-gray-50 rounded-lg p-3">
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full {getActionColor(selectedLog.action)}">
|
<span class="px-2 py-1 text-xs font-semibold rounded-full {getActionColor(selectedLog.action)}">
|
||||||
{selectedLog.action}
|
{selectedLog.action}
|
||||||
</span>
|
</span>
|
||||||
<p class="mt-2 text-sm text-gray-700">{formatActionWithRole(selectedLog)}</p>
|
<p class="mt-2 text-sm text-gray-700">{formatActionText(selectedLog.action)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -660,7 +730,7 @@
|
|||||||
<!-- User Agent -->
|
<!-- User Agent -->
|
||||||
{#if selectedLog.user_agent}
|
{#if selectedLog.user_agent}
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-gray-900 mb-2">User Agent</h4>
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Navegador / Dispositivo</h4>
|
||||||
<div class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 break-all">
|
<div class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 break-all">
|
||||||
{selectedLog.user_agent}
|
{selectedLog.user_agent}
|
||||||
</div>
|
</div>
|
||||||
@@ -668,7 +738,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Valores Anteriores -->
|
<!-- Valores Anteriores -->
|
||||||
{#if selectedLog.old_values}
|
{#if selectedLog.old_values && Object.keys(selectedLog.old_values).length > 0}
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-gray-900 mb-2">Valores Anteriores</h4>
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Valores Anteriores</h4>
|
||||||
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedLog.old_values, null, 2)}</pre>
|
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedLog.old_values, null, 2)}</pre>
|
||||||
@@ -676,7 +746,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Valores Nuevos -->
|
<!-- Valores Nuevos -->
|
||||||
{#if selectedLog.new_values}
|
{#if selectedLog.new_values && Object.keys(selectedLog.new_values).length > 0}
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-gray-900 mb-2">Valores Nuevos</h4>
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Valores Nuevos</h4>
|
||||||
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedLog.new_values, null, 2)}</pre>
|
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedLog.new_values, null, 2)}</pre>
|
||||||
@@ -684,9 +754,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Metadata -->
|
<!-- Metadata -->
|
||||||
{#if selectedLog.metadata}
|
{#if selectedLog.metadata && Object.keys(selectedLog.metadata).length > 0}
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-medium text-gray-900 mb-2">Metadata Adicional</h4>
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Información Adicional</h4>
|
||||||
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedLog.metadata, null, 2)}</pre>
|
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedLog.metadata, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user