Modernizar UI de registros de auditoría (UX mejorada)
- Diseño responsivo: tabla en desktop, tarjetas en móvil/tablet - Altura máxima con scroll interno (evita scroll de página completa) - Avatares circulares con iniciales del usuario - Filas/tarjetas más compactas y eficientes - Paginación sticky (siempre visible al fondo) - Botones primera/última página para navegación rápida - Página actual resaltada en color primary - Click en toda la fila para ver detalles - Diseño más moderno y limpio - Mejor uso del espacio vertical
This commit is contained in:
@@ -523,8 +523,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Logs -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden flex flex-col" style="max-height: calc(100vh - 500px); min-height: 400px;">
|
||||
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50 flex-shrink-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-900">
|
||||
Registros de Auditoría
|
||||
@@ -534,10 +534,14 @@
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center h-64">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p class="mt-2 text-sm text-gray-500">Cargando registros...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if logs.length === 0}
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<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">
|
||||
<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" />
|
||||
@@ -547,61 +551,72 @@
|
||||
{periodFilter === 'today' ? 'No hay actividad registrada hoy.' : 'No se encontraron registros para el período seleccionado.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<!-- Contenedor con scroll -->
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<!-- Vista Desktop (Tabla) -->
|
||||
<div class="hidden lg:block">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<thead class="bg-gray-50 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
|
||||
Hora
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Usuario
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Acción
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Recurso
|
||||
</th>
|
||||
<th scope="col" class="relative px-4 py-3">
|
||||
<th scope="col" class="relative px-3 py-2 w-20">
|
||||
<span class="sr-only">Acciones</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each logs as log (log.id)}
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
<tr class="hover:bg-gray-50 transition-colors cursor-pointer" on:click={() => viewDetail(log)}>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatDateShort(log.created_at)}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<td class="px-3 py-2 text-sm">
|
||||
{#if log.user_email}
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">{log.user_name || 'N/A'}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-xs font-medium text-primary-700">
|
||||
{(log.user_name || '?').charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-medium text-gray-900 truncate">{log.user_name || 'N/A'}</div>
|
||||
<div class="text-xs text-gray-500">{getRoleText(log.user_role)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-gray-500 italic">Sistema</span>
|
||||
<span class="text-gray-500 italic text-sm">Sistema</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full {getActionColor(log.action)}">
|
||||
{formatActionText(log.action)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<div class="text-gray-900 font-medium">{log.resource_type}</div>
|
||||
<td class="px-3 py-2 text-sm">
|
||||
<div class="font-medium text-gray-900">{log.resource_type}</div>
|
||||
{#if log.ip_address}
|
||||
<div class="text-xs text-gray-500">{log.ip_address}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-right text-sm">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-right text-sm">
|
||||
<button
|
||||
on:click={() => viewDetail(log)}
|
||||
on:click|stopPropagation={() => viewDetail(log)}
|
||||
class="text-primary-600 hover:text-primary-900 font-medium"
|
||||
>
|
||||
Detalle
|
||||
Ver
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -610,69 +625,157 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
<!-- Vista Mobile/Tablet (Tarjetas) -->
|
||||
<div class="lg:hidden divide-y divide-gray-200">
|
||||
{#each logs as log (log.id)}
|
||||
<div
|
||||
class="p-4 hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
on:click={() => viewDetail(log)}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex items-start gap-3 flex-1 min-w-0">
|
||||
<!-- Avatar -->
|
||||
{#if log.user_email}
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-primary-700">
|
||||
{(log.user_name || '?').charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-shrink-0 w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<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="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Contenido -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs text-gray-500">{formatDateShort(log.created_at)}</span>
|
||||
<span class="px-2 py-0.5 text-xs font-medium rounded-full {getActionColor(log.action)}">
|
||||
{formatActionText(log.action)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="font-medium text-gray-900 text-sm mb-1">
|
||||
{log.user_name || 'Sistema'}
|
||||
<span class="text-xs text-gray-500 font-normal ml-1">• {getRoleText(log.user_role)}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{log.resource_type}
|
||||
{#if log.ip_address}
|
||||
<span class="text-xs text-gray-400 ml-1">• {log.ip_address}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botón Ver -->
|
||||
<button
|
||||
on:click|stopPropagation={() => viewDetail(log)}
|
||||
class="flex-shrink-0 text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paginación (Sticky al fondo) -->
|
||||
{#if totalPages > 1}
|
||||
<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="bg-white border-t border-gray-200 px-4 py-2 flex-shrink-0 sticky bottom-0">
|
||||
<!-- Mobile -->
|
||||
<div class="flex items-center justify-between sm:hidden">
|
||||
<button
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="relative inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<span class="text-sm text-gray-700">
|
||||
Página <span class="font-medium">{currentPage}</span> de <span class="font-medium">{totalPages}</span>
|
||||
</span>
|
||||
<button
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="relative inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Siguiente
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
Mostrando
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="hidden sm:flex sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-gray-700">
|
||||
<span class="font-medium">{(currentPage - 1) * perPage + 1}</span>
|
||||
a
|
||||
-
|
||||
<span class="font-medium">{Math.min(currentPage * perPage, totalLogs)}</span>
|
||||
de
|
||||
<span class="font-medium">{totalLogs}</span>
|
||||
resultados
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<!-- Botón Primera página -->
|
||||
{#if currentPage > 3}
|
||||
<button
|
||||
on:click={() => goToPage(1)}
|
||||
class="relative inline-flex items-center px-2 py-1.5 rounded-l-md border border-gray-300 bg-white text-xs font-medium text-gray-500 hover:bg-gray-50"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414zm-6 0a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L5.414 10l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Botón Anterior -->
|
||||
<button
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="relative inline-flex items-center px-2 py-1.5 {currentPage <= 3 ? 'rounded-l-md' : ''} border border-gray-300 bg-white text-xs font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span class="sr-only">Anterior</span>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Números de página -->
|
||||
{#each Array.from({length: Math.min(5, totalPages)}, (_, i) => i + Math.max(1, Math.min(currentPage - 2, totalPages - 4))) as page}
|
||||
<button
|
||||
on:click={() => goToPage(page)}
|
||||
class="relative inline-flex items-center px-4 py-2 border text-sm font-medium {page === currentPage ? 'z-10 bg-primary-50 border-primary-500 text-primary-600' : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'}"
|
||||
class="relative inline-flex items-center px-3 py-1.5 border text-xs font-medium transition-colors {page === currentPage ? 'z-10 bg-primary-600 border-primary-600 text-white' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'}"
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- Botón Siguiente -->
|
||||
<button
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="relative inline-flex items-center px-2 py-1.5 {currentPage >= totalPages - 2 ? 'rounded-r-md' : ''} border border-gray-300 bg-white text-xs font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span class="sr-only">Siguiente</span>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Botón Última página -->
|
||||
{#if currentPage < totalPages - 2}
|
||||
<button
|
||||
on:click={() => goToPage(totalPages)}
|
||||
class="relative inline-flex items-center px-2 py-1.5 rounded-r-md border border-gray-300 bg-white text-xs font-medium text-gray-500 hover:bg-gray-50"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10.293 15.707a1 1 0 010-1.414L14.586 10l-4.293-4.293a1 1 0 111.414-1.414l5 5a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd" d="M4.293 15.707a1 1 0 010-1.414L8.586 10 4.293 5.707a1 1 0 011.414-1.414l5 5a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user