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:
2026-02-16 08:31:07 -07:00
parent 51a8515b40
commit 90f9c9c6e6

View File

@@ -523,8 +523,8 @@
</div> </div>
<!-- Tabla de Logs --> <!-- Tabla de Logs -->
<div class="bg-white shadow rounded-lg overflow-hidden"> <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"> <div class="px-4 py-3 border-b border-gray-200 bg-gray-50 flex-shrink-0">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-900"> <h3 class="text-sm font-medium text-gray-900">
Registros de Auditoría Registros de Auditoría
@@ -534,145 +534,248 @@
</div> </div>
{#if isLoading} {#if isLoading}
<div class="flex items-center justify-center h-64"> <div class="flex items-center justify-center flex-1">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div> <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> </div>
{:else if logs.length === 0} {:else if logs.length === 0}
<div class="text-center py-12"> <div class="flex items-center justify-center flex-1">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div class="text-center py-12">
<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 class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</svg> <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" />
<h3 class="mt-2 text-sm font-medium text-gray-900">No hay registros</h3> </svg>
<p class="mt-1 text-sm text-gray-500"> <h3 class="mt-2 text-sm font-medium text-gray-900">No hay registros</h3>
{periodFilter === 'today' ? 'No hay actividad registrada hoy.' : 'No se encontraron registros para el período seleccionado.'} <p class="mt-1 text-sm text-gray-500">
</p> {periodFilter === 'today' ? 'No hay actividad registrada hoy.' : 'No se encontraron registros para el período seleccionado.'}
</p>
</div>
</div> </div>
{:else} {:else}
<div class="overflow-x-auto"> <!-- Contenedor con scroll -->
<table class="min-w-full divide-y divide-gray-200"> <div class="overflow-y-auto flex-1">
<thead class="bg-gray-50"> <!-- Vista Desktop (Tabla) -->
<tr> <div class="hidden lg:block">
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <table class="min-w-full divide-y divide-gray-200">
Hora <thead class="bg-gray-50 sticky top-0 z-10">
</th> <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">
Usuario Hora
</th> </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 Usuario
</th> </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 Acción
</th> </th>
<th scope="col" class="relative px-4 py-3"> <th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<span class="sr-only">Acciones</span> Recurso
</th> </th>
</tr> <th scope="col" class="relative px-3 py-2 w-20">
</thead> <span class="sr-only">Acciones</span>
<tbody class="bg-white divide-y divide-gray-200"> </th>
{#each logs as log (log.id)} </tr>
<tr class="hover:bg-gray-50 transition-colors"> </thead>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900"> <tbody class="bg-white divide-y divide-gray-200">
{formatDateShort(log.created_at)} {#each logs as log (log.id)}
</td> <tr class="hover:bg-gray-50 transition-colors cursor-pointer" on:click={() => viewDetail(log)}>
<td class="px-4 py-3 whitespace-nowrap text-sm"> <td class="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
{formatDateShort(log.created_at)}
</td>
<td class="px-3 py-2 text-sm">
{#if log.user_email}
<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 text-sm">Sistema</span>
{/if}
</td>
<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-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-3 py-2 whitespace-nowrap text-right text-sm">
<button
on:click|stopPropagation={() => viewDetail(log)}
class="text-primary-600 hover:text-primary-900 font-medium"
>
Ver
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- 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} {#if log.user_email}
<div> <div class="flex-shrink-0 w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
<div class="font-medium text-gray-900">{log.user_name || 'N/A'}</div> <span class="text-sm font-medium text-primary-700">
<div class="text-xs text-gray-500">{getRoleText(log.user_role)}</div> {(log.user_name || '?').charAt(0).toUpperCase()}
</span>
</div> </div>
{:else} {:else}
<span class="text-gray-500 italic">Sistema</span> <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} {/if}
</td>
<td class="px-4 py-3 whitespace-nowrap"> <!-- Contenido -->
<span class="px-2 py-1 text-xs font-medium rounded-full {getActionColor(log.action)}"> <div class="flex-1 min-w-0">
{formatActionText(log.action)} <div class="flex items-center gap-2 mb-1">
</span> <span class="text-xs text-gray-500">{formatDateShort(log.created_at)}</span>
</td> <span class="px-2 py-0.5 text-xs font-medium rounded-full {getActionColor(log.action)}">
<td class="px-4 py-3 whitespace-nowrap text-sm"> {formatActionText(log.action)}
<div class="text-gray-900 font-medium">{log.resource_type}</div> </span>
{#if log.ip_address} </div>
<div class="text-xs text-gray-500">{log.ip_address}</div> <div class="font-medium text-gray-900 text-sm mb-1">
{/if} {log.user_name || 'Sistema'}
</td> <span class="text-xs text-gray-500 font-normal ml-1">{getRoleText(log.user_role)}</span>
<td class="px-4 py-3 whitespace-nowrap text-right text-sm"> </div>
<button <div class="text-sm text-gray-600">
on:click={() => viewDetail(log)} {log.resource_type}
class="text-primary-600 hover:text-primary-900 font-medium" {#if log.ip_address}
> <span class="text-xs text-gray-400 ml-1">{log.ip_address}</span>
Detalle {/if}
</button> </div>
</td> </div>
</tr> </div>
{/each}
</tbody> <!-- Botón Ver -->
</table> <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> </div>
<!-- Paginación --> <!-- Paginación (Sticky al fondo) -->
{#if totalPages > 1} {#if totalPages > 1}
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200"> <div class="bg-white border-t border-gray-200 px-4 py-2 flex-shrink-0 sticky bottom-0">
<div class="flex-1 flex justify-between sm:hidden"> <!-- Mobile -->
<div class="flex items-center justify-between sm:hidden">
<button <button
on:click={() => goToPage(currentPage - 1)} on:click={() => goToPage(currentPage - 1)}
disabled={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 Anterior
</button> </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 <button
on:click={() => goToPage(currentPage + 1)} on:click={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages} 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 Siguiente
</button> </button>
</div> </div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div> <!-- Desktop -->
<p class="text-sm text-gray-700"> <div class="hidden sm:flex sm:items-center sm:justify-between">
Mostrando <div class="text-sm text-gray-700">
<span class="font-medium">{(currentPage - 1) * perPage + 1}</span> <span class="font-medium">{(currentPage - 1) * perPage + 1}</span>
a -
<span class="font-medium">{Math.min(currentPage * perPage, totalLogs)}</span> <span class="font-medium">{Math.min(currentPage * perPage, totalLogs)}</span>
de de
<span class="font-medium">{totalLogs}</span> <span class="font-medium">{totalLogs}</span>
resultados
</p>
</div> </div>
<div> <div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination"> <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 <button
on:click={() => goToPage(currentPage - 1)} on:click={() => goToPage(currentPage - 1)}
disabled={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-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<svg class="h-5 w-5" 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" /> <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> </svg>
</button> </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} {#each Array.from({length: Math.min(5, totalPages)}, (_, i) => i + Math.max(1, Math.min(currentPage - 2, totalPages - 4))) as page}
<button <button
on:click={() => goToPage(page)} 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} {page}
</button> </button>
{/each} {/each}
<!-- Botón Siguiente -->
<button <button
on:click={() => goToPage(currentPage + 1)} on:click={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages} 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-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<svg class="h-5 w-5" 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" /> <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> </svg>
</button> </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> </nav>
</div> </div>
</div> </div>