Se mejoro estetica y estandarizaron estilos

This commit is contained in:
2025-08-05 10:30:25 -06:00
parent c3d800ba48
commit aa515c1d01
12 changed files with 3530 additions and 1611 deletions

View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import Sidebar, { MobileMenuButton } from './Sidebar';
export default function ResponsiveSidebar({ children }) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const toggleMobileMenu = () => setIsMobileMenuOpen(prev => !prev);
const closeMobileMenu = () => setIsMobileMenuOpen(false);
return (
<div className="flex h-screen bg-gray-100">
{/* Sidebar */}
<Sidebar
isMobileOpen={isMobileMenuOpen}
onMobileClose={closeMobileMenu}
/>
{/* Contenido principal */}
<div className="flex-1 flex flex-col min-w-0">
{/* Header móvil - siempre visible en móviles */}
<header className="lg:hidden bg-white shadow-sm border-b border-gray-200 px-4 py-3 flex items-center justify-between sticky top-0 z-20">
<MobileMenuButton onClick={toggleMobileMenu} />
<h1 className="text-lg font-semibold text-gray-900">EFC Dashboard</h1>
<div className="w-8"></div> {/* Spacer para centrar el título */}
</header>
{/* Contenido */}
<main className="flex-1 overflow-y-auto p-4 lg:p-6">
{children}
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,115 @@
# Sidebar Responsivo - Instrucciones de Uso
El sidebar ha sido actualizado para ser completamente responsivo. Aquí tienes las opciones de implementación:
## Opción 1: Usar ResponsiveSidebar (Recomendado para nuevos proyectos)
```jsx
import ResponsiveSidebar from './components/ResponsiveSidebar';
function App() {
return (
<ResponsiveSidebar>
<h1>Contenido de tu aplicación</h1>
<p>Todo tu contenido va aquí...</p>
</ResponsiveSidebar>
);
}
```
**Características:**
- Header fijo en móviles con botón de menú
- Control completo del estado del sidebar
- Diseño consistente en todas las pantallas
## Opción 2: Sidebar Standalone (Para proyectos existentes)
```jsx
import Sidebar from './components/Sidebar';
function App() {
return (
<div className="flex h-screen">
<Sidebar /> {/* ¡Ahora funciona automáticamente en móviles! */}
<main className="flex-1 p-4">
Tu contenido aquí...
</main>
</div>
);
}
```
**Características:**
- Botón flotante automático en móviles (solo cuando es necesario)
- Funciona sin configuración adicional
- Mantiene compatibilidad con código existente
## Funcionalidades Responsivas
### Desktop (≥1024px)
- Sidebar fijo en el lado izquierdo
- Botón de colapsar/expandir
- Ancho: 256px (expandido) / 64px (colapsado)
### Móvil (<1024px)
#### Con ResponsiveSidebar:
- Header fijo con botón de menú siempre visible
- Sidebar se desliza desde la izquierda
- Overlay oscuro de fondo
#### Con Sidebar standalone:
- Botón flotante elegante en esquina superior izquierda (solo cuando está cerrado)
- Sidebar se desliza desde la izquierda al hacer clic
- Se oculta automáticamente al navegar
### Auto-cierre en móviles:
- Al hacer clic en el overlay
- Al navegar a otra página
- Al redimensionar la ventana a desktop
- Al hacer clic en el botón X
## Componentes Exportados
- `Sidebar`: Componente principal del sidebar
- `MobileMenuButton`: Botón para abrir el menú móvil
- `ResponsiveSidebar`: Wrapper completo con header móvil
## Props del Sidebar
```typescript
interface SidebarProps {
isMobileOpen?: boolean; // Estado del menú móvil (opcional)
onMobileClose?: () => void; // Función para cerrar el menú móvil (opcional)
}
```
**Nota:** Si no pasas estas props, el Sidebar manejará su propio estado automáticamente.
## Migración de Código Existente
### Si ya usas `<Sidebar />`:
**No necesitas cambiar nada!** El sidebar ahora funciona automáticamente en móviles.
### Si quieres el header móvil:
```jsx
// Cambia esto:
<div className="flex">
<Sidebar />
<main>Contenido</main>
</div>
// Por esto:
<ResponsiveSidebar>
Contenido
</ResponsiveSidebar>
```
## Estilos y Diseño
- **Botón flotante**: Diseño sutil que coincide con el tema del sidebar
- **Backdrop blur**: Efecto de cristal esmerilado en el botón
- **Transiciones suaves**: Animaciones consistentes
- **Z-index apropiado**: Sin conflictos con otros elementos
¡El sidebar es ahora completamente responsivo y funciona perfectamente en cualquier dispositivo! 📱💻

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useUser } from '../context/UserContext';
export default function Sidebar() {
export default function Sidebar({ isMobileOpen, onMobileClose }) {
// Leer si el usuario es importador desde localStorage
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
// Leer grupos del usuario desde localStorage
@@ -18,7 +18,15 @@ export default function Sidebar() {
const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5);
// Leer DEBUG_MODE desde variables de entorno
const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true';
// Estados para responsividad
const [isCollapsed, setIsCollapsed] = useState(false);
const [internalMobileOpen, setInternalMobileOpen] = useState(false);
// Usar estado interno si no se pasan props
const mobileOpen = isMobileOpen !== undefined ? isMobileOpen : internalMobileOpen;
const handleMobileClose = onMobileClose || (() => setInternalMobileOpen(false));
const handleMobileOpen = () => setInternalMobileOpen(true);
const location = useLocation();
const navigate = useNavigate();
const { user: currentUser, loading } = useUser();
@@ -30,6 +38,23 @@ export default function Sidebar() {
navigate('/login');
};
// Cerrar menú móvil cuando se navega o cuando la pantalla es grande
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) { // lg breakpoint
handleMobileClose();
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Cerrar menú móvil cuando cambia la ubicación
useEffect(() => {
handleMobileClose();
}, [location.pathname]);
// El usuario y loading ahora vienen del contexto global
// Definir todas las secciones
@@ -196,35 +221,79 @@ export default function Sidebar() {
.filter(Boolean);
return (
<div className={`bg-slate-900 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-screen flex flex-col shadow-xl`}>
{/* Header - Logo y colapsar */}
<div className="p-4 border-b border-slate-700 flex-shrink-0">
<div className="flex items-center justify-between">
{!isCollapsed && (
<div className="flex items-center">
{/* Logo de la organización */}
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3 shadow-lg">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<>
{/* Botón flotante para abrir menú en móvil - solo cuando se usa standalone */}
{!mobileOpen && isMobileOpen === undefined && (
<button
onClick={handleMobileOpen}
className="lg:hidden fixed top-4 left-4 z-30 p-2.5 bg-slate-900/95 backdrop-blur-sm text-white rounded-xl shadow-lg hover:bg-slate-800/95 transition-all duration-200 border border-slate-700/50"
aria-label="Abrir menú"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
)}
{/* Overlay para móviles */}
{mobileOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
onClick={handleMobileClose}
/>
)}
{/* Sidebar */}
<div className={`
bg-slate-900 text-white transition-all duration-300 flex flex-col shadow-xl
${isCollapsed ? 'w-16' : 'w-64'}
fixed lg:relative inset-y-0 left-0 z-50
${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
h-screen
`}>
{/* Header - Logo y colapsar */}
<div className="p-4 border-b border-slate-700 flex-shrink-0">
<div className="flex items-center justify-between">
{!isCollapsed && (
<div className="flex items-center">
{/* Logo de la organización */}
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3 shadow-lg">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
</div>
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
)}
{/* Botones de control */}
<div className="flex items-center space-x-2">
{/* Botón cerrar en móvil */}
<button
onClick={handleMobileClose}
className="lg:hidden p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Botón colapsar en desktop */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="hidden lg:block p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200 hover:shadow-md"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isCollapsed ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
)}
</svg>
</button>
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200 hover:shadow-md"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isCollapsed ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
)}
</svg>
</button>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 p-3 space-y-4 overflow-y-auto overflow-x-hidden">
@@ -404,6 +473,32 @@ export default function Sidebar() {
</div>
)}
</div>
</div>
</div>
</>
);
}
// Hook personalizado para manejar el menú móvil desde otros componentes
export function useMobileSidebar() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);
const close = () => setIsOpen(false);
return { isOpen, toggle, close };
}
// Componente botón para abrir menú móvil
export function MobileMenuButton({ onClick }) {
return (
<button
onClick={onClick}
className="lg:hidden p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors duration-200"
aria-label="Abrir menú"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
);
}

View File

@@ -87,123 +87,124 @@ export default function Admin() {
}
return (
<div className="p-6 bg-gray-50">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
{/* Header + Estado del Sistema alineados horizontalmente */}
<div className="mb-8 flex flex-col md:flex-row md:items-stretch md:gap-6">
<div className="mb-6 sm:mb-8 flex flex-col xl:flex-row xl:items-stretch gap-4 sm:gap-6">
{/* Header principal mejorado */}
<div className="relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6 flex-1 min-w-0 md:w-[65%] animate-fadein-slideup opacity-0"
<div className="relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 flex-1 min-w-0 xl:w-[65%] animate-fadein-slideup opacity-0"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards',
}}
>
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Panel de Administración
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
<span>Panel de Administración</span>
{services && (
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs sm:text-sm font-semibold px-3 py-1 rounded-full shadow-lg animate-fade-in">
{services.en_espera} en espera
</span>
)}
</h1>
<p className="text-lg text-blue-700/80 font-medium">
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">
{typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'
? 'Dashboard principal para gestión de Expediente electrónico'
: 'Dashboard principal para gestión de agencia aduanal'}
</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
</div>
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
</div>
{/* Animación personalizada para el icono y contador */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
animation: bounce-slow 3s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
animation: fade-in 0.8s ease-out;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-10px) rotate(2deg); }
66% { transform: translateY(-5px) rotate(-1deg); }
}
.animate-float {
animation: float 4s ease-in-out infinite;
}
`}</style>
</div>
{/* Estado del Sistema card a la derecha */}
<div className="mt-6 md:mt-0 md:w-[35%] min-w-[270px] flex-shrink-0 animate-fadein-slideup opacity-0"
<div className="xl:w-[35%] min-w-[280px] flex-shrink-0 animate-fadein-slideup opacity-0"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards',
}}
>
<div className="relative overflow-hidden rounded-2xl shadow bg-gradient-to-br from-green-50 via-white to-blue-50 border border-green-100 p-6 h-full flex flex-col justify-between">
<div className="relative overflow-hidden rounded-3xl shadow-2xl bg-white border border-gray-100 p-4 sm:p-6 h-full flex flex-col justify-between backdrop-blur-sm">
<div className="flex items-center gap-3 mb-4">
<div className="bg-green-100 rounded-full p-3 shadow-md animate-bounce-slow">
<svg className="h-7 w-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="bg-gradient-to-br from-emerald-500 to-green-600 rounded-full p-3 shadow-lg animate-bounce-slow">
<svg className="h-6 w-6 sm:h-7 sm:w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-2xl font-extrabold text-green-900 tracking-tight flex-1">Estado del Sistema</h3>
<h3 className="text-xl sm:text-2xl font-extrabold text-gray-900 tracking-tight flex-1">Estado del Sistema</h3>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-medium">API Backend</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1">
<div className="space-y-3 sm:space-y-4">
<div className="flex items-center justify-between p-3 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-100">
<span className="text-gray-700 font-medium text-sm sm:text-base">API Backend</span>
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1 shadow-sm">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
Conectado
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-700 font-medium">API Servicios</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1">
<div className="flex items-center justify-between p-3 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-100">
<span className="text-gray-700 font-medium text-sm sm:text-base">API Servicios</span>
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1 shadow-sm">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
Conectado
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-700 font-medium">Última Actualización</span>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800 border border-blue-200">
<div className="flex items-center justify-between p-3 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100">
<span className="text-gray-700 font-medium text-sm sm:text-base">Última Actualización</span>
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800 border border-blue-200 shadow-sm">
Hace 2 min
</span>
</div>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-8 -right-8 opacity-20 pointer-events-none select-none">
<svg width="80" height="80" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad2)" />
<defs>
<linearGradient id="grad2" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#22c55e" stopOpacity="0.18" />
<stop offset="1" stopColor="#3b82f6" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
{/* Efecto decorativo de fondo modernizado */}
<div className="absolute -top-8 -right-8 opacity-10 pointer-events-none select-none">
<div className="w-24 h-24 bg-gradient-to-br from-green-400 to-blue-500 rounded-full blur-xl"></div>
</div>
{/* Animación personalizada para el icono */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
animation: bounce-slow 3s infinite;
}
`}</style>
</div>
@@ -211,170 +212,290 @@ export default function Admin() {
</div>
{/* Stats Cards con datos de endpoints */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-6 sm:mb-8">
{/* Estados de servicios */}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 hover:shadow-2xl hover:scale-105 transition-all duration-500 transform animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards',
}}
>
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center shadow-md">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
</div>
<div className="ml-4 sm:ml-5 flex-1 min-w-0">
<p className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide">Procesos en Espera</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1">{services ? services.en_espera : '-'}</p>
<p className="text-xs sm:text-sm text-gray-400 mt-1">Total: <span className="font-semibold">{services ? services.procesos_filtrados : '-'}</span></p>
</div>
</div>
<div className="ml-5">
<p className="text-sm font-medium text-gray-500">Procesos en Espera</p>
<p className="text-2xl font-bold text-gray-900">{services ? services.en_espera : '-'}</p>
<p className="text-sm text-gray-400">Total: {services ? services.procesos_filtrados : '-'}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 hover:shadow-2xl hover:scale-105 transition-all duration-500 transform animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.35s forwards',
}}
>
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center shadow-md">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-green-500 to-green-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
<div className="ml-4 sm:ml-5 flex-1 min-w-0">
<p className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide">En Proceso</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1">{services ? services.en_proceso : '-'}</p>
<p className="text-xs sm:text-sm text-gray-400 mt-1">Finalizados: <span className="font-semibold">{services ? services.finalizados : '-'}</span></p>
</div>
</div>
<div className="ml-5">
<p className="text-sm font-medium text-gray-500">En Proceso</p>
<p className="text-2xl font-bold text-gray-900">{services ? services.en_proceso : '-'}</p>
<p className="text-sm text-gray-400">Finalizados: {services ? services.finalizados : '-'}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 hover:shadow-2xl hover:scale-105 transition-all duration-500 transform animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.45s forwards',
}}
>
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg flex items-center justify-center shadow-md">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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 className="absolute inset-0 bg-gradient-to-br from-orange-500/5 to-red-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-orange-500 to-orange-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
</div>
<div className="ml-4 sm:ml-5 flex-1 min-w-0">
<p className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide">Con Error</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1">{services ? services.con_error : '-'}</p>
<p className="text-xs sm:text-sm text-gray-400 mt-1">Finalizados: <span className="font-semibold">{services ? services.finalizados : '-'}</span></p>
</div>
</div>
<div className="ml-5">
<p className="text-sm font-medium text-gray-500">Con Error</p>
<p className="text-2xl font-bold text-gray-900">{services ? services.con_error : '-'}</p>
<p className="text-sm text-gray-400">Finalizados: {services ? services.finalizados : '-'}</p>
</div>
</div>
</div>
{/* Descargas */}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 hover:shadow-2xl hover:scale-105 transition-all duration-500 transform animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.55s forwards',
}}
>
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-gradient-to-br from-blue-700 to-blue-900 rounded-lg flex items-center justify-center shadow-md">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
</svg>
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/5 to-indigo-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
</svg>
</div>
</div>
<div className="ml-4 sm:ml-5 flex-1 min-w-0">
<p className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide">Descargados 1 día</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1">{downloads ? downloads.archivos_ultimas_1_dia : '-'}</p>
<div className="flex flex-wrap gap-1 mt-1">
<span className="text-xs text-gray-400">7 días: <span className="font-semibold">{downloads ? downloads.archivos_ultimos_7_dias : '-'}</span></span>
<span className="text-xs text-gray-400">| 30 días: <span className="font-semibold">{downloads ? downloads.archivos_ultimos_30_dias : '-'}</span></span>
</div>
</div>
</div>
<div className="ml-5">
<p className="text-sm font-medium text-gray-500">Descargados 1 día</p>
<p className="text-2xl font-bold text-gray-900">{downloads ? downloads.archivos_ultimas_1_dia : '-'}</p>
<p className="text-sm text-gray-400">7 días: {downloads ? downloads.archivos_ultimos_7_dias : '-'} | 30 días: {downloads ? downloads.archivos_ultimos_30_dias : '-'}</p>
</div>
</div>
</div>
</div>
{/* Análisis de actividad de usuario */}
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && !isGroup35 && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 mb-4 animate-fadein-slideup opacity-0"
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.65s forwards',
}}
>
<h3 className="text-lg font-bold text-gray-900 mb-4">Actividad de Usuarios</h3>
{loading ? (
<div className="text-gray-500">Cargando...</div>
) : error ? (
<div className="text-danger-600">{error}</div>
) : userActivity ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-semibold text-gray-700 mb-2">Resumen de acciones</h4>
<ul className="text-sm text-gray-700 space-y-1">
{Object.entries(userActivity.actions_count).map(([action, count]) => (
<li key={action} className="flex justify-between border-b border-gray-100 py-1">
<span className="capitalize">{action}</span>
<span className="font-mono text-blue-700">{count}</span>
</li>
))}
<li className="flex justify-between font-semibold pt-2">
<span>Total actividades</span>
<span className="font-mono text-blue-900">{userActivity.actividades_filtradas}</span>
</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-gray-700 mb-2">Top usuarios</h4>
<ol className="text-sm text-gray-700 space-y-1 list-decimal list-inside">
{userActivity.top_users.map((user, idx) => (
<li key={user.username} className="flex justify-between border-b border-gray-100 py-1">
<span>{user.username}</span>
<span className="font-mono text-green-700">{user.activity_count}</span>
</li>
))}
</ol>
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/3 to-purple-500/3"></div>
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4 sm:mb-6">
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full p-3 shadow-lg">
<svg className="h-6 w-6 sm:h-7 sm:w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">Actividad de Usuarios</h3>
</div>
) : null}
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
<span className="ml-3 text-gray-500">Cargando...</span>
</div>
) : error ? (
<div className="text-red-600 bg-red-50 p-4 rounded-xl border border-red-200">{error}</div>
) : userActivity ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 sm:gap-8">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-4 sm:p-6 border border-blue-100">
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
Resumen de acciones
</h4>
<div className="space-y-3">
{Object.entries(userActivity.actions_count).map(([action, count]) => (
<div key={action} className="flex justify-between items-center bg-white rounded-xl p-3 shadow-sm border border-blue-100">
<span className="capitalize text-gray-700 font-medium">{action}</span>
<span className="font-mono text-blue-700 bg-blue-100 px-2 py-1 rounded-lg text-sm font-bold">{count}</span>
</div>
))}
<div className="flex justify-between items-center bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl p-3 shadow-lg font-semibold">
<span>Total actividades</span>
<span className="font-mono bg-white/20 px-2 py-1 rounded-lg">{userActivity.actividades_filtradas}</span>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-2xl p-4 sm:p-6 border border-green-100">
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
Top usuarios
</h4>
<div className="space-y-3">
{userActivity.top_users.map((user, idx) => (
<div key={user.username} className="flex justify-between items-center bg-white rounded-xl p-3 shadow-sm border border-green-100">
<div className="flex items-center gap-3">
<span className="bg-green-100 text-green-800 rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold">{idx + 1}</span>
<span className="text-gray-700 font-medium">{user.username}</span>
</div>
<span className="font-mono text-green-700 bg-green-100 px-2 py-1 rounded-lg text-sm font-bold">{user.activity_count}</span>
</div>
))}
</div>
</div>
</div>
) : null}
</div>
</div>
)}
{/* Tabla de últimos documentos */}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 mb-8 animate-fadein-slideup opacity-0"
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 mb-6 sm:mb-8 animate-fadein-slideup opacity-0 relative overflow-hidden"
style={{
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.75s forwards',
}}
>
<h3 className="text-lg font-bold text-gray-900 mb-4">Últimos documentos agregados</h3>
{loading ? (
<div className="text-gray-500">Cargando...</div>
) : error ? (
<div className="text-danger-600">{error}</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left font-semibold text-gray-600">Archivo</th>
<th className="px-4 py-2 text-left font-semibold text-gray-600">Pedimento</th>
<th className="px-4 py-2 text-left font-semibold text-gray-600">Organización</th>
<th className="px-4 py-2 text-left font-semibold text-gray-600">Fecha</th>
</tr>
</thead>
<tbody>
{latestDocs.map(doc => (
<tr key={doc.id} className="hover:bg-blue-50">
<td className="px-4 py-2 font-mono text-blue-800 truncate max-w-xs" title={getFileName(doc.archivo)}>{getFileName(doc.archivo)}</td>
<td className="px-4 py-2">{doc.pedimento}</td>
<td className="px-4 py-2">{doc.organizacion}</td>
<td className="px-4 py-2">{new Date(doc.created_at).toLocaleString('es-MX')}</td>
</tr>
))}
</tbody>
</table>
<div className="absolute inset-0 bg-gradient-to-br from-slate-500/2 to-gray-500/3"></div>
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4 sm:mb-6">
<div className="bg-gradient-to-br from-slate-600 to-gray-700 rounded-full p-3 shadow-lg">
<svg className="h-6 w-6 sm:h-7 sm:w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">Últimos documentos agregados</h3>
</div>
)}
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-600"></div>
<span className="ml-3 text-gray-500">Cargando...</span>
</div>
) : error ? (
<div className="text-red-600 bg-red-50 p-4 rounded-xl border border-red-200">{error}</div>
) : (
<>
{/* Vista de tabla para pantallas grandes */}
<div className="hidden lg:block overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gradient-to-r from-gray-50 to-slate-50">
<tr>
<th className="px-4 sm:px-6 py-3 sm:py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider rounded-tl-xl">Archivo</th>
<th className="px-4 sm:px-6 py-3 sm:py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">Pedimento</th>
<th className="px-4 sm:px-6 py-3 sm:py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">Organización</th>
<th className="px-4 sm:px-6 py-3 sm:py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider rounded-tr-xl">Fecha</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100">
{latestDocs.map((doc, index) => (
<tr key={doc.id} className="hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 transition-all duration-200">
<td className="px-4 sm:px-6 py-3 sm:py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="bg-blue-100 rounded-lg p-2 mr-3">
<svg className="h-4 w-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div className="max-w-xs">
<div className="text-sm font-mono text-blue-800 truncate" title={getFileName(doc.archivo)}>
{getFileName(doc.archivo)}
</div>
</div>
</div>
</td>
<td className="px-4 sm:px-6 py-3 sm:py-4 whitespace-nowrap">
<span className="bg-gray-100 text-gray-800 px-2 py-1 rounded-lg text-sm font-semibold">{doc.pedimento}</span>
</td>
<td className="px-4 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-sm text-gray-700 font-medium">{doc.organizacion}</td>
<td className="px-4 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(doc.created_at).toLocaleString('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="lg:hidden space-y-4">
{latestDocs.map((doc, index) => (
<div key={doc.id} className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-2xl p-4 border border-gray-200 hover:shadow-lg transition-all duration-300">
<div className="flex items-start gap-3">
<div className="bg-blue-100 rounded-xl p-2 flex-shrink-0">
<svg className="h-5 w-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-mono text-blue-800 font-semibold mb-2 break-all" title={getFileName(doc.archivo)}>
{getFileName(doc.archivo)}
</div>
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-gray-500 font-medium">Pedimento:</span>
<span className="bg-gray-200 text-gray-800 px-2 py-1 rounded-lg text-xs font-semibold">{doc.pedimento}</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-gray-500 font-medium">Organización:</span>
<span className="text-sm text-gray-700 font-medium">{doc.organizacion}</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-gray-500 font-medium">Fecha:</span>
<span className="text-xs text-gray-600">
{new Date(doc.created_at).toLocaleString('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</>
)}
</div>
</div>

View File

@@ -267,35 +267,35 @@ export default function Documents() {
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
return (
<div className="p-6 bg-gray-50">
<div className="min-h-screen p-4 sm:p-6 bg-gradient-to-br from-gray-50 to-blue-50">
<div ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
<div className="max-w-7xl mx-auto">
{/* Header mejorado y decorativo */}
<div className={
"mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6"+
"mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 border border-blue-200 p-6 sm:p-8 flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6"+
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' } : undefined}>
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
<div className="flex-1">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
Documentos
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">{totalDocuments}</span>
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs font-semibold px-3 py-1 rounded-full animate-fade-in">{totalDocuments}</span>
</h1>
<p className="text-lg text-blue-700/80 font-medium">Descarga los documentos de tus pedimentos.</p>
<p className="text-sm sm:text-base lg:text-lg text-blue-100 font-medium">Descarga los documentos de tus pedimentos.</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
<stop stopColor="#1e40af" stopOpacity="0.15" />
<stop offset="1" stopColor="#1e3a8a" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
@@ -324,37 +324,37 @@ export default function Documents() {
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}>
<div className="px-6 py-6 border-b border-gray-200">
<div className="px-4 sm:px-6 py-6 border-b border-gray-200">
<div className="overflow-x-auto" id="tabla-documentos">
{/* Header de Documentos Relacionados arriba de los filtros */}
<div className="px-8 pt-8 pb-2 border-b border-gray-200">
<h2 className="text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
<div className="px-4 sm:px-6 lg:px-8 pt-6 sm:pt-8 pb-2 border-b border-gray-200">
<h2 className="text-xl sm:text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
Todos los Documentos
</h2>
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
</div>
{/* Filtros de query parameters */}
<div className="px-6 py-6 border-b border-gray-200">
<div className="px-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200">
{/* Filtros avanzados */}
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
<div className="mb-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Pedimento Número */}
<div className="flex flex-col flex-1 min-w-[150px]">
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento Número</label>
<input
type="text"
value={pedimentoNumeroFilter}
onChange={e => setPedimentoNumeroFilter(e.target.value)}
placeholder="Buscar por número de pedimento..."
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
placeholder="Buscar por número..."
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 transition-all"
/>
</div>
{/* Extensión */}
<div className="flex flex-col flex-1 min-w-[150px]">
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Extensión</label>
<select
value={extensionFilter}
onChange={e => setExtensionFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 transition-all"
>
<option value="">Todas</option>
<option value="pdf">PDF</option>
@@ -372,12 +372,12 @@ export default function Documents() {
</select>
</div>
{/* Tipo de documento */}
<div className="flex flex-col flex-1 min-w-[150px]">
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de documento</label>
<select
value={documentTypeFilter}
onChange={e => setDocumentTypeFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 transition-all"
>
<option value="">Todos</option>
<option value="1">Pedimento Partida</option>
@@ -389,13 +389,13 @@ export default function Documents() {
</select>
</div>
{/* Fecha de creación */}
<div className="flex flex-col flex-1 min-w-[150px]">
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de creación</label>
<input
type="date"
value={createdAtFilter}
onChange={e => setCreatedAtFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 transition-all"
/>
</div>
</div>
@@ -403,86 +403,218 @@ export default function Documents() {
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
{/* Botones de descarga */}
{currentDocuments.length > 0 && (
<div className="flex space-x-3 mb-2">
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<button
onClick={handleDownloadAll}
disabled={currentDocuments.length === 0}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
</svg>
Descargar todos
<span className="hidden sm:inline">Descargar todos</span>
<span className="sm:hidden">Todos</span>
</button>
<button
onClick={handleDownloadSelected}
disabled={selectedDocs.length === 0}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Descargar seleccionados ({selectedDocs.length})
<span className="hidden sm:inline">Descargar seleccionados ({selectedDocs.length})</span>
<span className="sm:hidden">Seleccionados ({selectedDocs.length})</span>
</button>
</div>
)}
</div>
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: currentDocuments.length > 6 ? 'auto' : 'hidden', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden text-xs">
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
<tr>
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">
</div>
{/* Vista responsiva: tabla para desktop, cards para mobile */}
{/* Tabla para pantallas grandes */}
<div className="hidden lg:block">
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: currentDocuments.length > 6 ? 'auto' : 'hidden', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden text-xs">
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
<tr>
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">
<input
type="checkbox"
checked={allSelected}
ref={el => { if (el) el.indeterminate = someSelected; }}
onChange={handleSelectAll}
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
style={{ minWidth: '14px', minHeight: '14px' }}
/>
</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Pedimento</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Archivo</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tamaño</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Extensión</th>
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Acciones</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
{loading ? (
<tr>
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-gray-500 text-lg">Cargando documentos...</span>
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-red-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
</div>
</td>
</tr>
) : currentDocuments.length > 0 ? (
<>
{currentDocuments.map(doc => (
<tr key={doc.id} className="transition-all duration-200 hover:bg-blue-50 hover:shadow-lg">
<td className="px-2 py-2 text-center align-middle">
<input
type="checkbox"
checked={selectedDocs.includes(doc.id)}
onChange={() => handleSelectOne(doc.id)}
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
style={{ minWidth: '14px', minHeight: '14px' }}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{doc.pedimento_numero}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{doc.archivo ? doc.archivo.split('/').pop() : ''}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{
(() => {
switch (String(doc.document_type)) {
case '1': return 'Pedimento Partida';
case '2': return 'Pedimento Completo';
case '3': return 'Pedimento Remesas';
case '4': return 'Pedimento Acuse';
case '5': return 'Pedimento EDocument';
case '6': return 'Estado Pedimento';
default: return doc.document_type || '';
}
})()
}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.size}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.extension}</td>
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
<button
className="inline-flex items-center px-3 py-1 border border-transparent text-xs font-semibold rounded-md text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow"
title="Descargar"
onClick={async () => {
await downloadFile(
doc.id,
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
() => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
},
null,
showMessage
);
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
</svg>
</button>
</td>
</tr>
))}
{/* Rellenar con filas vacías si hay menos de 8 */}
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
<tr key={`empty-${idx}`} className="">
<td className="px-2 py-4" />
<td className="px-6 py-4 whitespace-nowrap" colSpan={5}>&nbsp;</td>
</tr>
))}
</>
) : (
<tr>
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay documentos</h3>
<p className="text-gray-500">Aún no tienes documentos registrados.</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Cards para pantallas pequeñas */}
<div className="lg:hidden">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<span className="text-gray-500 text-lg">Cargando documentos...</span>
</div>
</div>
) : error ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="text-red-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
</div>
</div>
) : currentDocuments.length > 0 ? (
<div className="space-y-4">
{/* Selección múltiple en mobile */}
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={allSelected}
ref={el => { if (el) el.indeterminate = someSelected; }}
onChange={handleSelectAll}
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
style={{ minWidth: '14px', minHeight: '14px' }}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Pedimento</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Archivo</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tamaño</th>
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Extensión</th>
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Acciones</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
{loading ? (
<tr>
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-gray-500 text-lg">Cargando documentos...</span>
<span className="text-sm font-medium text-gray-700">Seleccionar todos</span>
</label>
<span className="text-sm text-gray-500">{selectedDocs.length} seleccionados</span>
</div>
{currentDocuments.map(doc => (
<div key={doc.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start space-x-3 mb-3">
<input
type="checkbox"
checked={selectedDocs.includes(doc.id)}
onChange={() => handleSelectOne(doc.id)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-0.5 flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900">
Pedimento: {doc.pedimento_numero}
</h3>
<p className="text-xs text-gray-500 mt-1 break-all">
{doc.archivo ? doc.archivo.split('/').pop() : ''}
</p>
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-danger-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
</div>
</td>
</tr>
) : currentDocuments.length > 0 ? (
<>
{currentDocuments.map(doc => (
<tr key={doc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
<td className="px-2 py-2 text-center align-middle">
<input
type="checkbox"
checked={selectedDocs.includes(doc.id)}
onChange={() => handleSelectOne(doc.id)}
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
style={{ minWidth: '14px', minHeight: '14px' }}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{doc.pedimento_numero}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{doc.archivo ? doc.archivo.split('/').pop() : ''}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{
</div>
<div className="grid grid-cols-2 gap-4 text-xs mb-4">
<div>
<span className="font-medium text-gray-500">Tipo:</span>
<p className="text-gray-900 mt-1">{
(() => {
switch (String(doc.document_type)) {
case '1': return 'Pedimento Partida';
@@ -494,150 +626,142 @@ export default function Documents() {
default: return doc.document_type || '';
}
})()
}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.size}</td>
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.extension}</td>
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
<button
className="inline-flex items-center px-3 py-1 border border-transparent text-xs font-semibold rounded-md text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow"
title="Descargar"
onClick={async () => {
await downloadFile(
doc.id,
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
() => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
},
null,
showMessage
);
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
</svg>
</button>
</td>
</tr>
))}
{/* Rellenar con filas vacías si hay menos de 8 */}
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
<tr key={`empty-${idx}`} className="">
<td className="px-2 py-4" />
<td className="px-6 py-4 whitespace-nowrap" colSpan={5}>&nbsp;</td>
</tr>
))}
</>
) : (
<tr>
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay pedimentos</h3>
<p className="text-gray-500">Aún no tienes pedimentos registrados.</p>
}</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
<div>
<span className="font-medium text-gray-500">Tamaño:</span>
<p className="text-gray-900 mt-1">{doc.size}</p>
</div>
<div>
<span className="font-medium text-gray-500">Extensión:</span>
<p className="text-gray-900 mt-1 uppercase">{doc.extension}</p>
</div>
</div>
<div className="pt-3 border-t border-gray-100">
<button
className="w-full inline-flex items-center justify-center px-3 py-2 border border-transparent text-xs font-semibold rounded-lg text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow"
onClick={async () => {
await downloadFile(
doc.id,
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
() => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
},
null,
showMessage
);
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
</svg>
Descargar Documento
</button>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12">
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay documentos</h3>
<p className="text-gray-500 text-center">Aún no tienes documentos registrados.</p>
</div>
)}
</div>
</div>
{/* Botón de actualizar eliminado por solicitud */}
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
</div>
<div className="overflow-hidden">
{/* Paginación con botones numerados y elipsis */}
{totalDocuments > 0 && (
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
{(() => {
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
const maxPagesToShow = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = startPage + maxPagesToShow - 1;
if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(i);
}
return (
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
<div className="flex items-center gap-2">
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
className="border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
>
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center gap-1 flex-wrap">
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
<div className="flex items-center gap-2">
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
className="border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
>
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center gap-1 flex-wrap">
<button
type="button"
onClick={e => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
«
</button>
<button
type="button"
onClick={e => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
</button>
{(() => {
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
const maxPagesToShow = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = startPage + maxPagesToShow - 1;
if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(i);
}
return pageNumbers.map(num => (
<button
type="button"
onClick={e => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === currentPage ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
disabled={num === currentPage}
>
«
{num}
</button>
<button
type="button"
onClick={e => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
</button>
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === currentPage ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
disabled={num === currentPage}
>
{num}
</button>
))}
<button
type="button"
onClick={e => handlePageChange(currentPage + 1, e)}
disabled={currentPage >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
</button>
<button
type="button"
onClick={e => handlePageChange(totalPages, e)}
disabled={currentPage >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
»
</button>
<span className="ml-3 text-xs text-gray-500">Página <span className="font-bold">{currentPage}</span> de <span className="font-bold">{totalPages}</span></span>
</div>
</div>
);
})()}
));
})()}
<button
type="button"
onClick={e => handlePageChange(currentPage + 1, e)}
disabled={currentPage >= Math.ceil(totalDocuments / itemsPerPage)}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= Math.ceil(totalDocuments / itemsPerPage)) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
</button>
<button
type="button"
onClick={e => handlePageChange(Math.ceil(totalDocuments / itemsPerPage), e)}
disabled={currentPage >= Math.ceil(totalDocuments / itemsPerPage)}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= Math.ceil(totalDocuments / itemsPerPage)) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
»
</button>
<span className="ml-3 text-xs text-gray-500">
Página <span className="font-bold">{currentPage}</span> de <span className="font-bold">{Math.ceil(totalDocuments / itemsPerPage)}</span>
</span>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>

View File

@@ -160,326 +160,349 @@ export default function Documents() {
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
return (
<div className="p-6 bg-gray-50">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
<div ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
<div className="max-w-7xl mx-auto">
{/* Header mejorado y decorativo */}
<div className={
"mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6"+
"mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6"+
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' } : undefined}>
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Expedientes
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">{totalDocuments}</span>
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
<span>Expedientes</span>
{totalDocuments > 0 && (
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs sm:text-sm font-semibold px-3 py-1 rounded-full shadow-lg animate-fade-in">
{totalDocuments} registros
</span>
)}
</h1>
<p className="text-lg text-blue-700/80 font-medium">Gestiona y descarga los documentos de tus pedimentos.</p>
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Gestiona y descarga los documentos de tus pedimentos</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
</div>
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
</div>
</div>
{/* Animación personalizada para el icono y contador */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
animation: bounce-slow 3s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
animation: fade-in 0.8s ease-out;
}
`}</style>
<div className={
"bg-white shadow-lg rounded-xl border border-gray-200"+
"bg-white shadow-2xl rounded-3xl border border-gray-100 overflow-hidden"+
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}>
<div className="px-6 py-6 border-b border-gray-200">
<div className="px-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-blue-50/30">
{/* Filtros avanzados */}
<div className="mb-4 flex flex-wrap gap-4 items-end">
{/* Search global */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Buscar</label>
<input
type="text"
value={searchFilter}
onChange={e => setSearchFilter(e.target.value)}
placeholder="Buscar pedimento, contribuyente, agente aduanal..."
className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
{/* Pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
<input
type="text"
value={pedimentoFilter}
onChange={e => setPedimentoFilter(e.target.value)}
placeholder="Buscar pedimento..."
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
{/* Alerta */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Alerta</label>
<select value={alertaFilter} onChange={e => setAlertaFilter(e.target.value)}
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50">
<option value="all">Todos</option>
<option value="true"></option>
<option value="false">No</option>
</select>
</div>
{/* Expediente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Expediente</label>
<select value={expedienteFilter} onChange={e => setExpedienteFilter(e.target.value)}
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50">
<option value="all">Todos</option>
<option value="true"></option>
<option value="false">No</option>
</select>
</div>
{/* Contribuyente combobox */}
<div className="flex flex-col relative">
<label className="text-xs font-semibold text-gray-700 mb-1">Contribuyente</label>
<input
type="text"
value={contribuyenteInput}
onChange={e => {
setContribuyenteInput(e.target.value);
setContribuyenteFilter('');
}}
placeholder="Buscar o escribir..."
className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
autoComplete="off"
/>
{/* Dropdown de sugerencias */}
{contribuyenteInput && (
<div className="absolute top-14 left-0 w-44 bg-white border border-gray-200 rounded-lg shadow-lg z-10 max-h-40 overflow-auto">
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
) : (
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
<button
key={c}
type="button"
className="w-full text-left px-3 py-2 hover:bg-blue-50 text-sm"
onClick={() => {
setContribuyenteFilter(c);
setContribuyenteInput('');
}}
>
{c}
</button>
))
)}
</div>
)}
</div>
{/* CURP Apoderado */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">CURP Apoderado</label>
<input
type="text"
value={curpApoderadoFilter}
onChange={e => setCurpApoderadoFilter(e.target.value)}
placeholder="CURP del apoderado..."
className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
{/* Fecha de pago */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de pago</label>
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" />
</div>
{/* Patente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Patente</label>
<input
type="text"
value={patenteFilter}
onChange={e => setPatenteFilter(e.target.value)}
placeholder="Patente..."
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
{/* Aduana */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Aduana</label>
<input
type="text"
value={aduanaFilter}
onChange={e => setAduanaFilter(e.target.value)}
placeholder="Aduana..."
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
{/* Tipo de operación */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de operación</label>
<input
type="text"
value={tipoOperacionFilter}
onChange={e => setTipoOperacionFilter(e.target.value)}
placeholder="ID tipo operación..."
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
{/* Clave pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Clave pedimento</label>
<input
type="text"
value={clavePedimentoFilter}
onChange={e => setClavePedimentoFilter(e.target.value)}
placeholder="Clave pedimento..."
className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
<div className="mb-4 sm:mb-6">
<h3 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">
<svg className="w-4 h-4 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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.414A1 1 0 013 6.707V4z" />
</svg>
Filtros de búsqueda
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
{/* Search global */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Buscar</label>
<input
type="text"
value={searchFilter}
onChange={e => setSearchFilter(e.target.value)}
placeholder="Buscar pedimento, contribuyente..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
{/* Pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Pedimento</label>
<input
type="text"
value={pedimentoFilter}
onChange={e => setPedimentoFilter(e.target.value)}
placeholder="Número de pedimento..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
{/* Expediente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Expediente</label>
<select value={expedienteFilter} onChange={e => setExpedienteFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md">
<option value="all">Todos</option>
<option value="true">Con expediente</option>
<option value="false">Sin expediente</option>
</select>
</div>
{/* Contribuyente combobox */}
<div className="flex flex-col relative">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Contribuyente</label>
<input
type="text"
value={contribuyenteInput}
onChange={e => {
setContribuyenteInput(e.target.value);
setContribuyenteFilter('');
}}
placeholder="Buscar o escribir..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
autoComplete="off"
/>
{/* Dropdown de sugerencias */}
{contribuyenteInput && (
<div className="absolute top-16 left-0 right-0 bg-white border border-gray-200 rounded-xl shadow-2xl z-50 max-h-40 overflow-auto">
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
) : (
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
<button
key={c}
type="button"
className="w-full text-left px-3 py-2 hover:bg-blue-50 text-sm transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
onClick={() => {
setContribuyenteFilter(c);
setContribuyenteInput('');
}}
>
{c}
</button>
))
)}
</div>
)}
</div>
{/* CURP Apoderado */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">CURP Apoderado</label>
<input
type="text"
value={curpApoderadoFilter}
onChange={e => setCurpApoderadoFilter(e.target.value)}
placeholder="CURP del apoderado..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
{/* Fecha de pago */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Fecha de pago</label>
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" />
</div>
{/* Patente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Patente</label>
<input
type="text"
value={patenteFilter}
onChange={e => setPatenteFilter(e.target.value)}
placeholder="Patente..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
{/* Aduana */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Aduana</label>
<input
type="text"
value={aduanaFilter}
onChange={e => setAduanaFilter(e.target.value)}
placeholder="Aduana..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
{/* Tipo de operación */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Tipo de operación</label>
<input
type="text"
value={tipoOperacionFilter}
onChange={e => setTipoOperacionFilter(e.target.value)}
placeholder="ID tipo operación..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
{/* Clave pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Clave pedimento</label>
<input
type="text"
value={clavePedimentoFilter}
onChange={e => setClavePedimentoFilter(e.target.value)}
placeholder="Clave pedimento..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
🔄 Actualización automática cada 30 segundos
</span>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<span className="inline-flex items-center text-xs text-blue-600 bg-blue-50 px-3 py-2 rounded-full font-medium">
<svg className="w-4 h-4 mr-2 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Actualización automática cada 30s
</span>
{loading && (
<span className="inline-flex items-center text-xs text-orange-600 bg-orange-50 px-3 py-2 rounded-full font-medium">
<svg className="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Actualizando...
</span>
)}
</div>
<button
onClick={refetch}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
disabled={loading}
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Actualizar Ahora
</button>
</div>
{success && (
<div className="mt-4 bg-green-50 border border-green-200 rounded-md p-4">
<div className="mt-4 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4 shadow-sm">
<div className="flex">
<svg className="h-5 w-5 text-green-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<p className="text-green-800">{success}</p>
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">{success}</p>
</div>
</div>
</div>
)}
</div>
<div className="overflow-hidden">
<div className="overflow-x-auto" id="tabla-documentos">
<div style={{ minHeight: 'calc(7 * 56px)', maxHeight: 'calc(7 * 56px)', overflowY: currentDocuments.length > 8 ? 'auto' : 'hidden', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
<thead className="bg-gray-50 sticky top-0 z-20 shadow">
{/* Vista de tabla para pantallas grandes */}
<div className="hidden lg:block">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gradient-to-r from-gray-50 to-blue-50">
<tr>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Pedimento</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Fecha de pago</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Contribuyente</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Alerta</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">CURP Apoderado</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Importe total</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Saldo disponible</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Importe pedimento</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expediente</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Pedimento</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Fecha de pago</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Contribuyente</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">CURP Apoderado</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Importe total</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Saldo disponible</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Importe pedimento</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Expediente</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
<tbody className="bg-white divide-y divide-gray-100">
{loading ? (
<tr>
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-gray-500 text-lg">Cargando documentos...</span>
<td colSpan={8} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
<span className="text-gray-500 text-lg font-medium">Cargando expedientes...</span>
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-danger-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
<td colSpan={8} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="bg-red-100 rounded-full p-3 mb-4">
<svg className="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<span className="text-red-600 text-lg font-medium">Error: {error.message || 'Error al cargar expedientes'}</span>
</div>
</td>
</tr>
) : currentDocuments.length > 0 ? (
<>
{currentDocuments.map(ped => (
<tr key={ped.id} className="hover:bg-blue-50 transition-all duration-300 hover:scale-[1.02] hover:shadow-md">
<td className="px-6 py-4 whitespace-nowrap">
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200"
>
{ped.pedimento}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.fechapago}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.contribuyente}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
ped.alerta
? 'bg-red-100 text-red-800'
: 'bg-green-100 text-green-800'
}`}>
{ped.alerta ? 'Sí' : 'No'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.curp_apoderado}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_total}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.saldo_disponible}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_pedimento}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
ped.existe_expediente
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{ped.existe_expediente ? 'Sí' : 'No'}
</span>
</td>
</tr>
))}
{/* Rellenar con filas vacías si hay menos de 8 */}
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
<tr key={`empty-${idx}`} className="">
<td className="px-6 py-4 whitespace-nowrap" colSpan={9}>&nbsp;</td>
</tr>
))}
</>
currentDocuments.map(ped => (
<tr key={ped.id} className="hover:bg-blue-50 transition-all duration-300 group">
<td className="px-4 py-4 whitespace-nowrap">
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-blue-600 hover:text-blue-800 font-semibold transition-colors duration-200 group-hover:underline"
>
{ped.pedimento}
</Link>
</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.fechapago}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700 max-w-xs truncate" title={ped.contribuyente}>{ped.contribuyente}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.curp_apoderado}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_total}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.saldo_disponible}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_pedimento}</td>
<td className="px-4 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
ped.existe_expediente
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-gray-100 text-gray-600 border border-gray-200'
}`}>
{ped.existe_expediente ? (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</>
) : (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
No
</>
)}
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<td colSpan={8} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="bg-gray-100 rounded-full p-4 mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay pedimentos</h3>
<p className="text-gray-500">Aún no tienes pedimentos registrados.</p>
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay expedientes</h3>
<p className="text-gray-500">No se encontraron expedientes con los filtros aplicados.</p>
</div>
</td>
</tr>
@@ -488,9 +511,97 @@ export default function Documents() {
</table>
</div>
</div>
{/* Paginación con botones numerados y elipsis */}
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="lg:hidden space-y-4 p-4">
{loading ? (
<div className="flex flex-col items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
<span className="text-gray-500 text-lg font-medium">Cargando expedientes...</span>
</div>
) : error ? (
<div className="flex flex-col items-center py-12">
<div className="bg-red-100 rounded-full p-3 mb-4">
<svg className="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<span className="text-red-600 text-lg font-medium">Error: {error.message || 'Error al cargar expedientes'}</span>
</div>
) : currentDocuments.length > 0 ? (
currentDocuments.map(ped => (
<div key={ped.id} className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 hover:shadow-xl transition-all duration-300 relative">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="bg-blue-100 rounded-xl p-2 flex-shrink-0">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div>
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-lg font-semibold text-blue-600 hover:text-blue-800 transition-colors duration-200"
>
{ped.pedimento}
</Link>
<p className="text-sm text-gray-500">{ped.fechapago}</p>
</div>
</div>
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold ${
ped.existe_expediente
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-gray-100 text-gray-600 border border-gray-200'
}`}>
{ped.existe_expediente ? 'Con expediente' : 'Sin expediente'}
</span>
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Contribuyente:</span>
<span className="text-sm text-gray-900 text-right max-w-[60%] truncate" title={ped.contribuyente}>
{ped.contribuyente}
</span>
</div>
{ped.curp_apoderado && (
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">CURP Apoderado:</span>
<span className="text-sm text-gray-900">{ped.curp_apoderado}</span>
</div>
)}
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center justify-between bg-green-50 rounded-lg p-2">
<span className="text-sm font-medium text-green-700">Importe total:</span>
<span className="text-sm font-bold text-green-800">${ped.importe_total}</span>
</div>
<div className="flex items-center justify-between bg-blue-50 rounded-lg p-2">
<span className="text-sm font-medium text-blue-700">Saldo disponible:</span>
<span className="text-sm font-bold text-blue-800">${ped.saldo_disponible}</span>
</div>
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-2">
<span className="text-sm font-medium text-gray-700">Importe pedimento:</span>
<span className="text-sm font-bold text-gray-800">${ped.importe_pedimento}</span>
</div>
</div>
</div>
</div>
))
) : (
<div className="bg-gray-50 rounded-2xl p-8 text-center">
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<p className="text-gray-500 font-medium">No hay expedientes disponibles</p>
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
</div>
)}
</div>
{/* Paginación moderna y responsiva */}
{totalDocuments > 0 && (
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
<div className="bg-gradient-to-r from-gray-50 to-blue-50/30 px-4 sm:px-6 py-4 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
{(() => {
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
const maxPagesToShow = 5;
@@ -505,26 +616,26 @@ export default function Documents() {
pageNumbers.push(i);
}
return (
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
<div className="flex items-center gap-2">
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-4">
<div className="flex items-center gap-3">
<label htmlFor="itemsPerPage" className="text-xs font-semibold text-gray-700">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
className="border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
className="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm"
>
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center gap-1 flex-wrap">
<div className="flex items-center justify-center sm:justify-end flex-1 gap-1">
<button
type="button"
onClick={e => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
>
«
</button>
@@ -532,26 +643,33 @@ export default function Documents() {
type="button"
onClick={e => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
>
</button>
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === currentPage ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
disabled={num === currentPage}
>
{num}
</button>
))}
<div className="hidden sm:flex items-center gap-1">
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${num === currentPage ? 'bg-blue-600 text-white border-blue-700 shadow-md cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
disabled={num === currentPage}
>
{num}
</button>
))}
</div>
<div className="sm:hidden flex items-center px-3 py-2 bg-white border border-gray-200 rounded-lg shadow-sm">
<span className="text-sm font-semibold text-gray-700">
{currentPage} / {totalPages}
</span>
</div>
<button
type="button"
onClick={e => handlePageChange(currentPage + 1, e)}
disabled={currentPage >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
>
</button>
@@ -559,11 +677,15 @@ export default function Documents() {
type="button"
onClick={e => handlePageChange(totalPages, e)}
disabled={currentPage >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
>
»
</button>
<span className="ml-3 text-xs text-gray-500">Página <span className="font-bold">{currentPage}</span> de <span className="font-bold">{totalPages}</span></span>
</div>
<div className="text-center sm:text-right">
<span className="text-xs text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-200 shadow-sm">
Mostrando <span className="font-bold text-blue-600">{((currentPage - 1) * itemsPerPage) + 1}</span> a <span className="font-bold text-blue-600">{Math.min(currentPage * itemsPerPage, totalDocuments)}</span> de <span className="font-bold text-blue-600">{totalDocuments}</span> registros
</span>
</div>
</div>
);

View File

@@ -67,202 +67,294 @@ export default function Organization() {
}, [info]);
if (loading) return (
<div className="h-full bg-gray-50 flex items-center justify-center">
<div className="text-center">
<svg className="animate-spin h-12 w-12 text-navy-600 mx-auto mb-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-gray-600 text-lg">Cargando información de la organización...</p>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="text-center bg-white rounded-3xl shadow-2xl border border-gray-100 p-8 sm:p-12 max-w-md mx-auto">
<div className="relative inline-block mb-6">
<svg className="animate-spin h-16 w-16 text-blue-600 mx-auto" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<div className="absolute inset-0 bg-blue-500/10 rounded-full blur-xl animate-pulse"></div>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Cargando información</h3>
<p className="text-gray-600">Obteniendo datos de la organización...</p>
</div>
</div>
);
if (error) return (
<div className="h-full bg-gray-50 flex items-center justify-center">
<div className="bg-danger-50 border border-danger-200 rounded-xl p-6 max-w-md shadow-lg">
<div className="flex items-center">
<svg className="h-6 w-6 text-danger-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p className="text-danger-800 font-medium">{error}</p>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="bg-white rounded-3xl shadow-2xl border border-red-100 p-8 sm:p-12 max-w-md mx-auto relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-red-500/5 to-orange-500/5"></div>
<div className="relative z-10 text-center">
<div className="bg-red-100 rounded-full p-4 w-16 h-16 mx-auto mb-6 flex items-center justify-center">
<svg className="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">Error al cargar</h3>
<p className="text-red-700 font-medium bg-red-50 rounded-xl p-3 border border-red-200">{error}</p>
</div>
</div>
</div>
);
return (
<div className="bg-gray-50 p-6">
<div className="max-w-6xl mx-auto">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
{/* Header mejorado y decorativo */}
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M3 7c0 2.21 3.582 4 8 4s8-1.79 8-4M3 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
</div>
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Mi Organización
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
<span>Mi Organización</span>
{info && (
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs sm:text-sm font-semibold px-3 py-1 rounded-full shadow-lg animate-fade-in">
{info.total_usuarios} usuarios
</span>
)}
</h1>
<p className="text-lg text-blue-700/80 font-medium">Información y métricas de uso de tu organización</p>
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Información y métricas de uso de tu organización</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
</div>
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
</div>
{/* Animación personalizada para el icono y contador */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
animation: bounce-slow 3s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
animation: fade-in 0.8s ease-out;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-10px) rotate(2deg); }
66% { transform: translateY(-5px) rotate(-1deg); }
}
.animate-float {
animation: float 4s ease-in-out infinite;
}
`}</style>
</div>
{/* Barra de almacenamiento con color y progress bar */}
<div className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<svg className="w-6 h-6 text-success-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
Uso de Almacenamiento
</h2>
<div className="relative w-full h-8 bg-gray-200 rounded-full overflow-hidden shadow-inner">
{/* Progress bar de color dinámico según porcentaje */}
{(() => {
const used = info?.espacio_utilizado_gb || 0;
const limit = info?.limite_almacenamiento_gb || 1;
const percent = Math.min(100, (100 * used / limit));
let barColor = 'linear-gradient(90deg, #22c55e 0%, #16a34a 100%)'; // verde
if (animatedPercent >= 80) {
barColor = 'linear-gradient(90deg, #ef4444 0%, #b91c1c 100%)'; // rojo
} else if (animatedPercent >= 50) {
barColor = 'linear-gradient(90deg, #f59e42 0%, #d97706 100%)'; // naranja
}
return (
<div
className="absolute left-0 top-0 h-8 rounded-full shadow-lg transition-all duration-700"
style={{ width: `${animatedPercent}%`, background: barColor }}
></div>
);
})()}
{/* Etiquetas sobre la barra */}
<div className="absolute left-0 top-0 w-full h-8 flex items-center justify-between px-4 text-sm font-semibold z-10">
<span className="text-success-700 flex items-center">
<span className="inline-block w-3 h-3 rounded-full bg-gradient-to-br from-green-400 to-green-600 mr-2"></span>
{info?.espacio_utilizado_gb?.toFixed(2)} GB usados
</span>
<span className="text-gray-700">{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB libres</span>
<span className="text-gray-500">{info?.limite_almacenamiento_gb} GB límite</span>
<div className="mb-6 sm:mb-10">
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 lg:p-8 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/3 to-blue-500/3"></div>
<div className="relative z-10">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-4 sm:mb-6 flex items-center">
<div className="bg-gradient-to-br from-green-500 to-emerald-600 rounded-full p-2 sm:p-3 shadow-lg mr-3">
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</div>
Uso de Almacenamiento
</h2>
{/* Estadísticas rápidas */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-2xl p-4 border border-green-100">
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-gray-600">Usado</span>
</div>
<div className="text-2xl font-bold text-green-700 mt-1">
{info?.espacio_utilizado_gb?.toFixed(2)} GB
</div>
</div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl p-4 border border-blue-100">
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium text-gray-600">Disponible</span>
</div>
<div className="text-2xl font-bold text-blue-700 mt-1">
{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB
</div>
</div>
<div className="bg-gradient-to-br from-gray-50 to-slate-50 rounded-2xl p-4 border border-gray-100">
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-gray-500 rounded-full"></div>
<span className="text-sm font-medium text-gray-600">Límite</span>
</div>
<div className="text-2xl font-bold text-gray-700 mt-1">
{info?.limite_almacenamiento_gb} GB
</div>
</div>
</div>
{/* Barra de progreso mejorada */}
<div className="relative w-full h-6 sm:h-8 bg-gray-200 rounded-full overflow-hidden shadow-inner">
{/* Progress bar de color dinámico según porcentaje */}
{(() => {
const used = info?.espacio_utilizado_gb || 0;
const limit = info?.limite_almacenamiento_gb || 1;
const percent = Math.min(100, (100 * used / limit));
let barColor = 'linear-gradient(90deg, #22c55e 0%, #16a34a 100%)'; // verde
if (animatedPercent >= 80) {
barColor = 'linear-gradient(90deg, #ef4444 0%, #b91c1c 100%)'; // rojo
} else if (animatedPercent >= 50) {
barColor = 'linear-gradient(90deg, #f59e42 0%, #d97706 100%)'; // naranja
}
return (
<div
className="absolute left-0 top-0 h-full rounded-full shadow-lg transition-all duration-700 flex items-center justify-end pr-3"
style={{ width: `${animatedPercent}%`, background: barColor }}
>
{animatedPercent > 20 && (
<span className="text-white font-bold text-xs sm:text-sm">
{animatedPercent.toFixed(1)}%
</span>
)}
</div>
);
})()}
{/* Indicador de porcentaje fuera de la barra si es muy pequeña */}
{animatedPercent <= 20 && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<span className="text-gray-600 font-bold text-xs sm:text-sm">
{animatedPercent.toFixed(1)}%
</span>
</div>
)}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 gap-4 sm:gap-6">
{/* Tarjeta Organización */}
<div className="bg-white rounded-lg shadow-md border border-navy-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '0ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-700 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M3 7c0 2.21 3.582 4 8 4s8-1.79 8-4M3 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '0ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-blue-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M3 7c0 2.21 3.582 4 8 4s8-1.79 8-4M3 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Organización</span>
<span className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 text-center break-words">{info?.organizacion}</span>
</div>
<span className="text-navy-700 font-semibold mb-1">Organización</span>
<span className="text-2xl font-bold text-navy-900">{info?.organizacion}</span>
</div>
{/* Tarjeta Usuarios */}
<div className="bg-white rounded-lg shadow-md border border-primary-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '50ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-700 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m9-4a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '50ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-green-500 to-green-700 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m9-4a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Usuarios</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.total_usuarios}</span>
</div>
<span className="text-primary-700 font-semibold mb-1">Usuarios</span>
<span className="text-2xl font-bold text-primary-900">{info?.total_usuarios}</span>
</div>
{/* Tarjeta Pedimentos */}
<div className="bg-white rounded-lg shadow-md border border-success-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '100ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-orange-400 to-orange-600 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-6a2 2 0 012-2h2a2 2 0 012 2v6m-4 0h4" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '100ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-orange-500 to-orange-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Pedimentos</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.total_pedimentos}</span>
</div>
<span className="text-success-700 font-semibold mb-1">Pedimentos</span>
<span className="text-2xl font-bold text-success-900">{info?.total_pedimentos}</span>
</div>
{/* Tarjeta Documentos */}
<div className="bg-white rounded-lg shadow-md border border-warning-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '150ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 7h10M7 11h10M7 15h6M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '150ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-yellow-500/5 to-amber-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 7h10M7 11h10M7 15h6M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Documentos</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.total_documentos}</span>
</div>
<span className="text-warning-700 font-semibold mb-1">Documentos</span>
<span className="text-2xl font-bold text-warning-900">{info?.total_documentos}</span>
</div>
{/* Tarjeta Límite de Almacenamiento */}
<div className="bg-white rounded-lg shadow-md border border-light-gray-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '200ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-gray-400 to-gray-600 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '200ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-gray-500/5 to-slate-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-gray-500 to-gray-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Límite de Almacenamiento</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.limite_almacenamiento_gb} GB</span>
</div>
<span className="text-light-gray-700 font-semibold mb-1">Límite de Almacenamiento</span>
<span className="text-2xl font-bold text-light-gray-900">{info?.limite_almacenamiento_gb} GB</span>
</div>
{/* Tarjeta Espacio Utilizado */}
<div className="bg-white rounded-lg shadow-md border border-warning-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '250ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-pink-400 to-pink-600 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v8m0 0a4 4 0 100-8 4 4 0 000 8z" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '250ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-pink-500/5 to-rose-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-pink-500 to-pink-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Espacio Utilizado</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.espacio_utilizado_gb?.toFixed(2)} GB</span>
</div>
<span className="text-warning-700 font-semibold mb-1">Espacio Utilizado</span>
<span className="text-2xl font-bold text-warning-900">{info?.espacio_utilizado_gb?.toFixed(2)} GB</span>
</div>
{/* Tarjeta Espacio Disponible */}
<div className="bg-white rounded-lg shadow-md border border-success-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '300ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-green-400 to-green-600 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 20V4m0 0a8 8 0 110 16 8 8 0 010-16z" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '300ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-green-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-emerald-500 to-green-600 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Espacio Disponible</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB</span>
</div>
<span className="text-success-700 font-semibold mb-1">Espacio Disponible</span>
<span className="text-2xl font-bold text-success-900">{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB</span>
</div>
{/* Tarjeta Porcentaje Utilizado */}
<div className="bg-white rounded-lg shadow-md border border-accent-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '350ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="w-12 h-12 bg-gradient-to-br from-purple-400 to-purple-700 rounded-lg flex items-center justify-center shadow-md mb-2">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" strokeWidth="2" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6l4 2" />
</svg>
<div className="group bg-white rounded-2xl shadow-lg border border-gray-100 p-4 sm:p-6 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-105 hover:shadow-2xl relative overflow-hidden" style={{ animationDelay: '350ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-blue-600/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative z-10 w-full flex flex-col items-center">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-shadow duration-300 mb-3">
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" strokeWidth="2" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6l4 2" />
</svg>
</div>
<span className="text-xs sm:text-sm font-medium text-gray-500 uppercase tracking-wide text-center mb-2">Porcentaje Utilizado</span>
<span className="text-2xl sm:text-3xl font-bold text-gray-900">{info?.porcentaje_utilizado}%</span>
</div>
<span className="text-accent-700 font-semibold mb-1">Porcentaje Utilizado</span>
<span className="text-2xl font-bold text-accent-900">{info?.porcentaje_utilizado}%</span>
</div>
</div>

View File

@@ -64,9 +64,14 @@ export default function Procesos() {
}
try {
const token = localStorage.getItem('access');
if (!token) {
alert('No hay token de autenticación. Por favor, inicia sesión nuevamente.');
setExecutingId(null);
return;
}
const headers = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
'Authorization': `Bearer ${token}`,
};
const body = JSON.stringify({
pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento,
@@ -90,14 +95,19 @@ export default function Procesos() {
// Cierra el dropdown si se hace click fuera
useEffect(() => {
if (openDropdownId === null) return;
function handleClick(e) {
function handleClickOutside(e) {
const el = document.getElementById(`dropdown-acciones-${openDropdownId}`);
if (el && !el.contains(e.target)) {
setOpenDropdownId(null);
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
// Usar setTimeout para evitar que el click que abre el dropdown lo cierre inmediatamente
setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 100);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [openDropdownId]);
useEffect(() => {
@@ -106,6 +116,10 @@ export default function Procesos() {
setError('');
try {
const token = localStorage.getItem('access');
if (!token) {
setError('No hay token de autenticación. Por favor, inicia sesión nuevamente.');
return;
}
// Construir query params
const params = new URLSearchParams();
params.append('page', String(page));
@@ -117,10 +131,21 @@ export default function Procesos() {
params.append('ordering', (sortOrder === 'desc' ? '-' : '') + sortField);
}
const API_URL = import.meta.env.VITE_EFC_API_URL;
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
console.log('Fetching procesos with token:', token ? 'Token present' : 'No token');
console.log('URL:', `${API_URL}/customs/procesamientopedimentos/?${params.toString()}`);
const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`, { headers });
if (!res.ok) throw new Error('Error al obtener procesamiento de pedimentos');
console.log('Response status:', res.status);
if (!res.ok) {
const errorText = await res.text();
console.log('Error response:', errorText);
throw new Error(`Error al obtener procesamiento de pedimentos: ${res.status} - ${errorText}`);
}
const data = await res.json();
console.log('Data received:', data);
setProcesos(data.results || []);
setCount(data.count || 0);
} catch (err) {
@@ -133,41 +158,48 @@ export default function Procesos() {
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]);
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{/* Header mejorado y responsivo */}
<div className="mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 animate-fadein-slideup opacity-0"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div>
<h1 className="text-3xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Procesos del Sistema
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in" title="Total de procesos">
{count}
</span>
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
<span>Procesos del Sistema</span>
{count > 0 && (
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs sm:text-sm font-semibold px-3 py-1 rounded-full shadow-lg animate-fade-in">
{count} procesos
</span>
)}
</h1>
<p className="text-lg text-blue-700/80 font-medium">Estado actual de los procesos de la agencia aduanal</p>
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Estado actual de los procesos de la agencia aduanal</p>
</div>
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
</div>
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
</div>
{/* Animaciones CSS */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
animation: bounce-slow 3s infinite;
}
@keyframes fadein-slideup {
0% { opacity: 0; transform: translateY(40px); }
@@ -177,200 +209,440 @@ export default function Procesos() {
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
animation: fade-in 0.8s ease-out;
}
`}</style>
</div>
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
<div className="flex items-center justify-between mb-2">
<h2 className="text-2xl font-bold text-blue-800">Procesamiento de Pedimentos</h2>
{/* Contenido principal */}
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 lg:p-8 animate-fadein-slideup opacity-0"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl p-2 shadow-lg">
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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>
Procesamiento de Pedimentos
</h2>
{count > 0 && (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl px-4 py-2 border border-blue-100">
<span className="text-sm font-medium text-blue-700">Total de registros: </span>
<span className="text-lg font-bold text-blue-800">{count}</span>
</div>
)}
</div>
{/* Filtros */}
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
<div className="flex flex-col flex-1 min-w-[150px]">
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
<input
type="text"
value={pedimentoPedimentoFilter}
onChange={e => setPedimentoPedimentoFilter(e.target.value)}
placeholder="Buscar por pedimento..."
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
<div className="flex flex-col flex-1 min-w-[150px]">
<label className="text-xs font-semibold text-gray-700 mb-1">Estado</label>
<select
value={estadoFilter}
onChange={e => setEstadoFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
>
<option value="">Todos</option>
<option value="1">En Espera</option>
<option value="2">Procesando</option>
<option value="3">Finalizado</option>
<option value="4">Error</option>
</select>
</div>
<div className="flex flex-col flex-1 min-w-[150px]">
<label className="text-xs font-semibold text-gray-700 mb-1">Servicio</label>
<select
value={servicioFilter}
onChange={e => setServicioFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
>
<option value="">Todos</option>
<option value="1">Estado de pedimento</option>
<option value="2">Listado de pedimentos</option>
<option value="3">Pedimento Completo</option>
<option value="4">Pedimento Partidas</option>
<option value="5">Pedimento Remesas</option>
<option value="6">Acuse</option>
<option value="7">EDocument</option>
<option value="8">Cove</option>
<option value="9">Acuse Cove</option>
</select>
{/* Filtros responsivos mejorados */}
<div className="mb-6 bg-gradient-to-r from-gray-50 to-slate-50 rounded-2xl p-4 sm:p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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.414A1 1 0 013 6.707V4z" />
</svg>
Filtros de búsqueda
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
Pedimento
</label>
<input
type="text"
value={pedimentoPedimentoFilter}
onChange={e => setPedimentoPedimentoFilter(e.target.value)}
placeholder="Buscar por pedimento..."
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
Estado
</label>
<select
value={estadoFilter}
onChange={e => setEstadoFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
>
<option value="">Todos los estados</option>
<option value="1">En Espera</option>
<option value="2">Procesando</option>
<option value="3">Finalizado</option>
<option value="4">Error</option>
</select>
</div>
<div className="space-y-2 sm:col-span-2 lg:col-span-1">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
Servicio
</label>
<select
value={servicioFilter}
onChange={e => setServicioFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
>
<option value="">Todos los servicios</option>
<option value="1">Estado de pedimento</option>
<option value="2">Listado de pedimentos</option>
<option value="3">Pedimento Completo</option>
<option value="4">Pedimento Partidas</option>
<option value="5">Pedimento Remesas</option>
<option value="6">Acuse</option>
<option value="7">EDocument</option>
<option value="8">Cove</option>
<option value="9">Acuse Cove</option>
</select>
</div>
</div>
</div>
{/* Estados de carga y error mejorados */}
{loading ? (
<div className="text-center text-gray-500 py-8">Cargando procesos...</div>
<div className="flex flex-col items-center justify-center py-12">
<div className="relative">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600"></div>
<div className="absolute inset-0 bg-blue-500/10 rounded-full blur-xl animate-pulse"></div>
</div>
<p className="mt-4 text-gray-600 font-medium">Cargando procesos...</p>
</div>
) : error ? (
<div className="text-center text-danger-600 py-8">{error}</div>
<div className="bg-red-50 border border-red-200 rounded-2xl p-6 text-center">
<div className="bg-red-100 rounded-full p-3 w-12 h-12 mx-auto mb-4 flex items-center justify-center">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-red-800 mb-2">Error al cargar</h3>
<p className="text-red-600">{error}</p>
</div>
) : (
<div className="overflow-x-auto">
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: 'auto', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
<>
{/* Vista de tabla para pantallas grandes */}
<div className="hidden lg:block overflow-x-auto bg-white rounded-2xl border border-gray-200 shadow-sm">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gradient-to-r from-gray-50 to-slate-50">
<tr>
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
<th className="px-4 py-4 text-center text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200 rounded-tl-2xl"
onClick={() => {
setSortField('id');
setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')}
<div className="flex items-center justify-center gap-1">
ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('organizacion_name');
setSortOrder(sortField === 'organizacion_name' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')}
<div className="flex items-center gap-1">
Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('estado');
setSortOrder(sortField === 'estado' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')}
<div className="flex items-center gap-1">
Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('pedimento');
setSortOrder(sortField === 'pedimento' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')}
<div className="flex items-center gap-1">
Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap cursor-pointer select-none"
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('servicio');
setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')}
<div className="flex items-center gap-1">
Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">
<th className="px-4 py-4 text-center text-xs font-bold text-gray-600 uppercase tracking-wider rounded-tr-2xl">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(12 * 40px)' }}>
{procesos.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-8 text-gray-500">No hay registros</td>
</tr>
) : (
procesos.map((proc) => (
<tr key={proc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">{proc.id}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{proc.organizacion_name || '-'}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{
proc.estado === 1 ? 'En Espera'
: proc.estado === 2 ? 'Procesando'
: proc.estado === 3 ? 'Finalizado'
: proc.estado === 4 ? 'Error'
: String(proc.estado)
}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{
typeof proc.pedimento === 'object' && proc.pedimento !== null
? proc.pedimento.pedimento || JSON.stringify(proc.pedimento)
: proc.pedimento
}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{
proc.servicio === 1 ? 'Estado de pedimento'
: proc.servicio === 2 ? 'Listado de pedimentos'
: proc.servicio === 3 ? 'Pedimento Completo'
: proc.servicio === 4 ? 'Pedimento Partidas'
: proc.servicio === 5 ? 'Pedimento Remesas'
: proc.servicio === 6 ? 'Acuse'
: proc.servicio === 7 ? 'EDocument'
: proc.servicio === 8 ? 'Cove'
: proc.servicio === 9 ? 'Acuse Cove'
: String(proc.servicio)
}</td>
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">
<div className="relative inline-block text-left" id={`dropdown-acciones-${proc.id}`}>
<button
className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-3 py-1 bg-white text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
type="button"
onClick={() => setOpenDropdownId(openDropdownId === proc.id ? null : proc.id)}
>
Acciones
<svg className="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
</button>
{openDropdownId === proc.id && (
<div className="absolute right-0 mt-2 w-32 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-20">
<div className="py-1">
<button
className="block w-full text-left px-4 py-2 text-xs text-blue-700 hover:bg-blue-100 disabled:opacity-60"
onClick={() => handleEjecutarServicio(proc)}
disabled={
executingId === proc.id ||
proc.estado === 2 || // Procesando
proc.estado === 3 || // Finalizado
proc.estado === 4 // Error
}
>
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
</button>
<button
className={`block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100${(proc.estado === 2 || proc.estado === 4) ? '' : ' opacity-50 cursor-not-allowed'}`}
disabled={!(proc.estado === 2 || proc.estado === 4)}
>
Pasar a espera
</button>
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Editar</button>
</div>
</div>
)}
<tbody className="bg-white divide-y divide-gray-100">
{procesos.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-12">
<div className="flex flex-col items-center">
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<p className="text-gray-500 font-medium">No hay procesos disponibles</p>
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
</div>
</td>
</tr>
))
)}
</tbody>
) : (
procesos.map((proc) => (
<tr key={proc.id} className="transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 hover:shadow-lg">
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
<span className="bg-gray-100 text-gray-800 px-2 py-1 rounded-lg text-sm font-semibold">{proc.id}</span>
</td>
<td className="px-4 py-4 whitespace-nowrap align-middle text-sm font-medium text-gray-900">{proc.organizacion_name || '-'}</td>
<td className="px-4 py-4 whitespace-nowrap align-middle">
{(() => {
const estado = proc.estado === 1 ? { text: 'En Espera', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' }
: proc.estado === 2 ? { text: 'Procesando', color: 'bg-blue-100 text-blue-800 border-blue-200' }
: proc.estado === 3 ? { text: 'Finalizado', color: 'bg-green-100 text-green-800 border-green-200' }
: proc.estado === 4 ? { text: 'Error', color: 'bg-red-100 text-red-800 border-red-200' }
: { text: String(proc.estado), color: 'bg-gray-100 text-gray-800 border-gray-200' };
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold border ${estado.color}`}>
{estado.text}
</span>
);
})()}
</td>
<td className="px-4 py-4 whitespace-nowrap align-middle text-sm text-gray-900 font-mono">
{typeof proc.pedimento === 'object' && proc.pedimento !== null
? proc.pedimento.pedimento || JSON.stringify(proc.pedimento)
: proc.pedimento}
</td>
<td className="px-4 py-4 whitespace-nowrap align-middle text-sm text-gray-700">
{proc.servicio === 1 ? 'Estado de pedimento'
: proc.servicio === 2 ? 'Listado de pedimentos'
: proc.servicio === 3 ? 'Pedimento Completo'
: proc.servicio === 4 ? 'Pedimento Partidas'
: proc.servicio === 5 ? 'Pedimento Remesas'
: proc.servicio === 6 ? 'Acuse'
: proc.servicio === 7 ? 'EDocument'
: proc.servicio === 8 ? 'Cove'
: proc.servicio === 9 ? 'Acuse Cove'
: String(proc.servicio)}
</td>
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
<div className="relative inline-block text-left z-30" id={`dropdown-acciones-${proc.id}`}>
<button
className="inline-flex justify-center items-center rounded-xl border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 active:bg-gray-100"
type="button"
onClick={() => setOpenDropdownId(openDropdownId === proc.id ? null : proc.id)}
>
Acciones
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{openDropdownId === proc.id && (
<div className="absolute right-0 mt-2 w-48 rounded-xl shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-[9999] border border-gray-200">
<div className="py-2">
<button
className="flex items-center w-full text-left px-4 py-3 text-sm text-blue-700 hover:bg-blue-50 disabled:opacity-60 disabled:cursor-not-allowed transition-colors duration-200"
onClick={() => {
handleEjecutarServicio(proc);
setOpenDropdownId(null); // Cerrar dropdown después de ejecutar
}}
disabled={
executingId === proc.id ||
proc.estado === 2 || // Procesando
proc.estado === 3 || // Finalizado
proc.estado === 4 // Error
}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h8m2 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" />
</svg>
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
</button>
<button
className={`flex items-center w-full text-left px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200 ${(proc.estado === 2 || proc.estado === 4) ? '' : ' opacity-50 cursor-not-allowed'}`}
disabled={!(proc.estado === 2 || proc.estado === 4)}
onClick={() => {
setOpenDropdownId(null); // Cerrar dropdown
// Aquí iría la lógica para pasar a espera
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Pasar a espera
</button>
<button
className="flex items-center w-full text-left px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200"
onClick={() => {
setOpenDropdownId(null); // Cerrar dropdown
// Aquí iría la lógica para editar
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Editar
</button>
</div>
</div>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Paginación igual a Documents.jsx */}
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="lg:hidden space-y-4">
{procesos.length === 0 ? (
<div className="bg-gray-50 rounded-2xl p-8 text-center">
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<p className="text-gray-500 font-medium">No hay procesos disponibles</p>
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
</div>
) : (
procesos.map((proc) => (
<div key={proc.id} className={`bg-white rounded-2xl shadow-lg border border-gray-200 p-4 hover:shadow-xl transition-all duration-300 relative ${openDropdownId === proc.id ? 'z-[100]' : ''}`}>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="bg-blue-100 rounded-xl p-2 flex-shrink-0">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="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" />
</svg>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-lg font-semibold text-gray-900">Proceso #{proc.id}</h3>
<p className="text-sm text-gray-500">{proc.organizacion_name || 'Sin organización'}</p>
</div>
</div>
{(() => {
const estado = proc.estado === 1 ? { text: 'En Espera', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' }
: proc.estado === 2 ? { text: 'Procesando', color: 'bg-blue-100 text-blue-800 border-blue-200' }
: proc.estado === 3 ? { text: 'Finalizado', color: 'bg-green-100 text-green-800 border-green-200' }
: proc.estado === 4 ? { text: 'Error', color: 'bg-red-100 text-red-800 border-red-200' }
: { text: String(proc.estado), color: 'bg-gray-100 text-gray-800 border-gray-200' };
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold border ${estado.color}`}>
{estado.text}
</span>
);
})()}
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Pedimento:</span>
<span className="text-sm font-mono text-gray-900 bg-gray-100 px-2 py-1 rounded">
{typeof proc.pedimento === 'object' && proc.pedimento !== null
? proc.pedimento.pedimento || JSON.stringify(proc.pedimento)
: proc.pedimento}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Servicio:</span>
<span className="text-sm text-gray-900 text-right max-w-[60%]">
{proc.servicio === 1 ? 'Estado de pedimento'
: proc.servicio === 2 ? 'Listado de pedimentos'
: proc.servicio === 3 ? 'Pedimento Completo'
: proc.servicio === 4 ? 'Pedimento Partidas'
: proc.servicio === 5 ? 'Pedimento Remesas'
: proc.servicio === 6 ? 'Acuse'
: proc.servicio === 7 ? 'EDocument'
: proc.servicio === 8 ? 'Cove'
: proc.servicio === 9 ? 'Acuse Cove'
: String(proc.servicio)}
</span>
</div>
</div>
<div className="relative z-30" id={`dropdown-acciones-${proc.id}`}>
<button
className="w-full inline-flex justify-center items-center rounded-xl border border-gray-300 shadow-sm px-4 py-3 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 active:bg-gray-100"
type="button"
onClick={() => setOpenDropdownId(openDropdownId === proc.id ? null : proc.id)}
>
Acciones
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{openDropdownId === proc.id && (
<div className="absolute left-0 right-0 mt-2 rounded-xl shadow-2xl bg-white ring-2 ring-gray-300 z-[9999] border border-gray-200 overflow-hidden"
style={{ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)' }}>
<div className="py-1">
<button
className="flex items-center w-full text-left px-4 py-4 text-sm text-blue-700 hover:bg-blue-50 disabled:opacity-60 disabled:cursor-not-allowed transition-colors duration-200 border-b border-gray-100"
onClick={() => {
handleEjecutarServicio(proc);
setOpenDropdownId(null); // Cerrar dropdown después de ejecutar
}}
disabled={
executingId === proc.id ||
proc.estado === 2 || // Procesando
proc.estado === 3 || // Finalizado
proc.estado === 4 // Error
}
>
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h8m2 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" />
</svg>
<span className="font-medium">
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
</span>
</button>
<button
className={`flex items-center w-full text-left px-4 py-4 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200 border-b border-gray-100 ${(proc.estado === 2 || proc.estado === 4) ? '' : ' opacity-50 cursor-not-allowed'}`}
disabled={!(proc.estado === 2 || proc.estado === 4)}
onClick={() => {
setOpenDropdownId(null); // Cerrar dropdown
// Aquí iría la lógica para pasar a espera
}}
>
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Pasar a espera</span>
</button>
<button
className="flex items-center w-full text-left px-4 py-4 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200"
onClick={() => {
setOpenDropdownId(null); // Cerrar dropdown
// Aquí iría la lógica para editar
}}
>
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span className="font-medium">Editar</span>
</button>
</div>
</div>
)}
</div>
</div>
))
)}
</div>
{/* Paginación compartida mejorada */}
{count > 0 && (
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
<div className="bg-gradient-to-r from-gray-50 to-slate-50 px-4 sm:px-6 py-4 mt-6 rounded-2xl border border-gray-200 flex flex-col sm:flex-row items-center justify-between gap-4">
{(() => {
const totalPages = Math.max(1, Math.ceil(count / itemsPerPage));
const maxPagesToShow = 5;
@@ -385,26 +657,26 @@ export default function Procesos() {
pageNumbers.push(i);
}
return (
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
<div className="flex items-center gap-2">
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
<>
<div className="flex items-center gap-3">
<label htmlFor="itemsPerPage" className="text-sm text-gray-600 font-medium">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
className="border border-gray-300 rounded px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
className="border border-gray-300 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm"
>
{[5, 8, 12, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center gap-1 flex-wrap">
<div className="flex items-center gap-2">
<button
type="button"
onClick={e => { e.preventDefault(); setPage(1); }}
disabled={page === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`}
>
«
</button>
@@ -412,7 +684,7 @@ export default function Procesos() {
type="button"
onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }}
disabled={page === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`}
>
</button>
@@ -421,7 +693,7 @@ export default function Procesos() {
type="button"
key={num}
onClick={e => { e.preventDefault(); setPage(num); }}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === page ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${num === page ? 'bg-blue-600 text-white border-blue-700 cursor-default shadow-lg' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`}
disabled={num === page}
>
{num}
@@ -431,7 +703,7 @@ export default function Procesos() {
type="button"
onClick={e => { e.preventDefault(); setPage(p => p + 1); }}
disabled={page >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`}
>
</button>
@@ -439,18 +711,20 @@ export default function Procesos() {
type="button"
onClick={e => { e.preventDefault(); setPage(totalPages); }}
disabled={page >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`}
>
»
</button>
<span className="ml-3 text-xs text-gray-500">Página <span className="font-bold">{page}</span> de <span className="font-bold">{totalPages}</span></span>
</div>
</div>
<span className="text-sm text-gray-600">
Página <span className="font-bold text-gray-800">{page}</span> de <span className="font-bold text-gray-800">{totalPages}</span>
</span>
</>
);
})()}
</div>
)}
</div>
</>
)}
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -332,37 +332,40 @@ export default function Users() {
const activeCount = users.filter(u => u.is_active === true).length;
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header Mejorado */}
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6">
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<div className="min-h-screen p-4 sm:p-6 bg-gradient-to-br from-gray-50 to-blue-50">
<div className="max-w-7xl mx-auto">
{/* Header Mejorado */}
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 border border-blue-200 p-6 sm:p-8 flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6">
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div className="flex-1">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
Usuarios
<div className="flex flex-wrap gap-2">
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs font-semibold px-3 py-1 rounded-full animate-fade-in">{users.length}</span>
<span className="inline-block bg-green-500/80 backdrop-blur-sm text-white text-xs font-semibold px-3 py-1 rounded-full animate-fade-in" title="Usuarios activos">
Activos: {activeCount}
</span>
</div>
</h1>
<p className="text-sm sm:text-base lg:text-lg text-blue-100 font-medium">Gestiona y supervisa los usuarios registrados en el sistema.</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#1e40af" stopOpacity="0.15" />
<stop offset="1" stopColor="#1e3a8a" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
</div>
</div>
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Usuarios
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">{users.length}</span>
<span className="inline-block bg-lime-200 text-lime-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in" title="Usuarios activos">
Activos: {activeCount}
</span>
</h1>
<p className="text-lg text-blue-700/80 font-medium">Gestiona y supervisa los usuarios registrados en el sistema.</p>
</div>
{/* Efecto decorativo de fondo */}
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
</div>
</div>
{/* Animación personalizada para el icono */}
<style>{`
@keyframes bounce-slow {
@@ -381,73 +384,81 @@ export default function Users() {
}
`}</style>
{/* Stats Cards con animación */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white overflow-hidden shadow rounded-lg fade-in-up-users transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Total Usuarios</dt>
<dd className="text-lg font-medium text-gray-900">{users.length}</dd>
</dl>
{/* Stats Cards con animación */}
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-8">
<div className="bg-white overflow-hidden shadow-lg rounded-xl fade-in-up-users transition-all duration-300 hover:scale-105 hover:shadow-2xl cursor-pointer border border-gray-100">
<div className="p-4 sm:p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
<div className="ml-4 w-0 flex-1">
<dl>
<dt className="text-xs sm:text-sm font-medium text-gray-500 truncate">Total</dt>
<dd className="text-lg sm:text-xl font-bold text-gray-900">{users.length}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg fade-in-up-users transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer" style={{ animationDelay: '0.08s' }}>
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Activos</dt>
<dd className="text-lg font-medium text-gray-900">{activeCount}</dd>
</dl>
<div className="bg-white overflow-hidden shadow-lg rounded-xl fade-in-up-users transition-all duration-300 hover:scale-105 hover:shadow-2xl cursor-pointer border border-gray-100" style={{ animationDelay: '0.08s' }}>
<div className="p-4 sm:p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div className="ml-4 w-0 flex-1">
<dl>
<dt className="text-xs sm:text-sm font-medium text-gray-500 truncate">Activos</dt>
<dd className="text-lg sm:text-xl font-bold text-gray-900">{activeCount}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg fade-in-up-users transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer" style={{ animationDelay: '0.16s' }}>
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Con Perfil Completo</dt>
<dd className="text-lg font-medium text-gray-900">
{users.filter(u => u.first_name && u.last_name).length}
</dd>
</dl>
<div className="bg-white overflow-hidden shadow-lg rounded-xl fade-in-up-users transition-all duration-300 hover:scale-105 hover:shadow-2xl cursor-pointer border border-gray-100" style={{ animationDelay: '0.16s' }}>
<div className="p-4 sm:p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
<div className="ml-4 w-0 flex-1">
<dl>
<dt className="text-xs sm:text-sm font-medium text-gray-500 truncate">Completos</dt>
<dd className="text-lg sm:text-xl font-bold text-gray-900">
{users.filter(u => u.first_name && u.last_name).length}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg fade-in-up-users transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer" style={{ animationDelay: '0.24s' }}>
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-6 w-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Última Semana</dt>
<dd className="text-lg font-medium text-gray-900">
<div className="bg-white overflow-hidden shadow-lg rounded-xl fade-in-up-users transition-all duration-300 hover:scale-105 hover:shadow-2xl cursor-pointer border border-gray-100" style={{ animationDelay: '0.24s' }}>
<div className="p-4 sm:p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<svg className="h-6 w-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div className="ml-4 w-0 flex-1">
<dl>
<dt className="text-xs sm:text-sm font-medium text-gray-500 truncate">Recientes</dt>
<dd className="text-lg sm:text-xl font-bold text-gray-900">
{users.filter(u => u.id % 3 === 0).length}
</dd>
</dl>
@@ -457,90 +468,93 @@ export default function Users() {
</div>
</div>
{/* Search and Actions */}
<div className="bg-white shadow rounded-lg mb-6">
<div className="px-6 py-4 border-b border-gray-200">
{/* Barra de búsqueda principal */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4">
<div className="flex-1 min-w-0 max-w-md">
<div className="relative bg-gray-50 rounded-lg border border-gray-200 shadow-sm">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:bg-white block w-full pl-10 pr-10 py-3 sm:text-sm border-0 bg-transparent rounded-lg placeholder-gray-500 transition-all duration-200"
placeholder="Buscar por nombre, email, estado..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
title="Busca por: nombre de usuario, email, nombre completo, estado (activo, inactivo, admin)"
/>
{searchTerm && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
onClick={() => setSearchTerm('')}
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-200"
title="Limpiar búsqueda"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Search and Actions */}
<div className="bg-white shadow-lg rounded-xl mb-6 border border-gray-100">
<div className="px-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200">
{/* Barra de búsqueda principal */}
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-4">
<div className="flex-1 min-w-0 max-w-lg">
<div className="relative bg-gray-50 rounded-lg border border-gray-200 shadow-sm">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
)}
<input
type="text"
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:bg-white block w-full pl-10 pr-10 py-3 sm:text-sm border-0 bg-transparent rounded-lg placeholder-gray-500 transition-all duration-200"
placeholder="Buscar por nombre, email, estado..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
title="Busca por: nombre de usuario, email, nombre completo, estado (activo, inactivo, admin)"
/>
{searchTerm && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
onClick={() => setSearchTerm('')}
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-200"
title="Limpiar búsqueda"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<button
onClick={() => { setShowCreateModal(true); setCreateType('agente'); }}
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span className="hidden sm:inline">Nuevo Agente</span>
<span className="sm:hidden">Agente</span>
</button>
<button
onClick={() => { setShowCreateModal(true); setCreateType('importador'); }}
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span className="hidden sm:inline">Nuevo Importador</span>
<span className="sm:hidden">Importador</span>
</button>
</div>
{/* Modal para crear usuario (agente o importador) eliminado */}
</div>
<div className="mt-4 sm:mt-0 sm:ml-4 flex gap-2">
<button
onClick={() => { setShowCreateModal(true); setCreateType('agente'); }}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Nuevo Agente
</button>
<button
onClick={() => { setShowCreateModal(true); setCreateType('importador'); }}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Nuevo Importador
</button>
</div>
{/* Modal para crear usuario (agente o importador) eliminado */}
</div>
{/* Filtros avanzados */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700">Filtrar por:</span>
<div className="flex flex-wrap gap-2">
{/* Filtros avanzados */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex flex-col space-y-3">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700">Filtrar por:</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
{[
{ key: 'all', label: 'Todos', count: users.length },
{ key: 'agente', label: 'Agente Aduanal', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(3) && u.groups.includes(4) : false).length },
{ key: 'importador', label: 'Importador', count: users.filter(u => (Array.isArray(u.groups) && u.groups.includes(3) && u.groups.includes(6)) || u.is_importador === true).length },
{ key: 'admin', label: 'Administrador', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(1) && u.groups.includes(3) && u.groups.includes(4) : false).length },
{ key: 'admin', label: 'Admin', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(1) && u.groups.includes(3) && u.groups.includes(4) : false).length },
{ key: 'developer', label: 'Developer', count: users.filter(u => Array.isArray(u.groups) ? u.groups.includes(2) && u.groups.includes(3) && u.groups.includes(4) : false).length },
{ key: 'inactive', label: 'Inactivos', count: users.filter(u => u.is_active === false).length }
].map(filter => (
<button
key={filter.key}
onClick={() => setStatusFilter(filter.key)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-200 ${statusFilter === filter.key
? 'bg-blue-100 text-blue-800 border-2 border-blue-300 shadow-sm'
: 'bg-white text-gray-700 border-2 border-gray-200 hover:bg-gray-50 hover:border-gray-300'
className={`inline-flex flex-col items-center justify-center px-3 py-2 rounded-lg text-xs font-medium transition-all duration-200 ${statusFilter === filter.key
? 'bg-blue-100 text-blue-800 border-2 border-blue-300 shadow-sm transform scale-105'
: 'bg-white text-gray-700 border-2 border-gray-200 hover:bg-gray-50 hover:border-gray-300 hover:scale-105'
}`}
>
{filter.label}
<span className={`ml-1.5 px-1.5 py-0.5 rounded-full text-xs font-semibold ${statusFilter === filter.key ? 'bg-blue-200 text-blue-900' : 'bg-gray-200 text-gray-600'
<span className="truncate">{filter.label}</span>
<span className={`mt-1 px-2 py-0.5 rounded-full text-xs font-semibold ${statusFilter === filter.key ? 'bg-blue-200 text-blue-900' : 'bg-gray-200 text-gray-600'
}`}>
{filter.count}
</span>
@@ -549,7 +563,6 @@ export default function Users() {
</div>
</div>
</div>
</div>
{/* Información de resultados */}
{(debouncedSearchTerm || statusFilter !== 'all') && (
@@ -585,17 +598,19 @@ export default function Users() {
)}
</div>
{/* Table con animación y layout SPA fijo */}
<div className="overflow-hidden">
{/* Vista responsiva: tabla para desktop, cards para mobile */}
{/* Tabla para pantallas grandes */}
<div className="hidden lg:block bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100">
<div style={{ minHeight: 'calc(8 * 56px)', maxHeight: 'calc(8 * 56px)', overflowY: 'auto', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden">
<thead className="bg-gray-50 sticky top-0 z-20">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gradient-to-r from-gray-50 to-blue-50 sticky top-0 z-20">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Usuario</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Estado</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nombre Completo</th>
<th scope="col" className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Usuario</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Email</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Estado</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Nombre Completo</th>
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
@@ -612,7 +627,7 @@ export default function Users() {
<tr>
<td colSpan={5} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-danger-600 text-lg">Error: {error}</span>
<span className="text-red-600 text-lg">Error: {error}</span>
</div>
</td>
</tr>
@@ -622,7 +637,7 @@ export default function Users() {
<tr
key={user.id}
className={
`transition-all duration-300 hover:scale-[1.025] hover:shadow-lg hover:bg-blue-50 fade-in-up-users` +
`transition-all duration-300 hover:scale-[1.015] hover:shadow-md hover:bg-blue-50 fade-in-up-users` +
(idx % 2 === 0 ? ' bg-white' : ' bg-gray-50')
}
style={{ animationDelay: `${0.05 * idx}s` }}
@@ -680,7 +695,7 @@ export default function Users() {
</span>
)}
{(user.is_staff || user.is_superuser) && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1zM5.618 4.504a1 1 0 01-.372 1.364L5.016 6l.23.132a1 1 0 11-.992 1.736L3 7.723V8a1 1 0 01-2 0V6a.996.996 0 01.52-.878l1.734-.99a1 1 0 011.364.372zm8.764 0a1 1 0 011.364-.372l1.733.99A1.002 1.002 0 0118 6v2a1 1 0 11-2 0v-.277l-1.254.145a1 1 0 11-.992-1.736L14.984 6l-.23-.132a1 1 0 01-.372-1.364zm-7 4a1 1 0 011.364-.372L10 8.848l1.254-.716a1 1 0 11.992 1.736L11 10.723V12a1 1 0 11-2 0v-1.277l-1.246-.855a1 1 0 01-.372-1.364zM3 11a1 1 0 011 1v1.277l1.246.855a1 1 0 11-.992 1.736l-1.75-1A1 1 0 012 14v-2a1 1 0 011-1zm14 0a1 1 0 011 1v2a1.002 1.002 0 01-.504.868l-1.75 1a1 1 0 11-.992-1.736L16 13.277V12a1 1 0 011-1zm-9.618 5.504a1 1 0 011.364-.372l.254.145V16a1 1 0 112 0v.277l.254-.145a1 1 0 11.992 1.736l-1.735.992a.995.995 0 01-1.022 0l-1.735-.992a1 1 0 01-.372-1.364z" clipRule="evenodd" />
</svg>
@@ -700,11 +715,10 @@ export default function Users() {
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<div className="flex justify-center space-x-2">
{/* Botón Editar eliminado */}
<button
onClick={() => { setShowDeleteModal(true); setUserToDelete(user); }}
disabled={user.username === localStorage.getItem('username')}
className={`inline-flex items-center px-3 py-1.5 border border-red-300 shadow-sm text-xs font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors ${user.username === localStorage.getItem('username') ? 'opacity-50 cursor-not-allowed' : ''}`}
className={`inline-flex items-center px-3 py-1.5 border border-red-300 shadow-sm text-xs font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 transform hover:scale-105 ${user.username === localStorage.getItem('username') ? 'opacity-50 cursor-not-allowed' : ''}`}
title={user.username === localStorage.getItem('username') ? 'No puedes eliminar tu propia cuenta' : 'Eliminar usuario'}
>
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -715,7 +729,6 @@ export default function Users() {
</div>
</td>
</tr>
))}
{/* Rellenar con filas vacías si hay menos de 8 */}
{currentUsers.length < 8 && !loading && !error && Array.from({ length: 8 - currentUsers.length }).map((_, idx) => (
@@ -744,79 +757,169 @@ export default function Users() {
</div>
</div>
{/* Paginación */}
{totalUsers > 0 && (
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
<div className="flex items-center mb-4 sm:mb-0">
<span className="text-sm text-gray-700 mr-4">
Mostrando <span className="font-semibold">{startIndex + 1}</span> - <span className="font-semibold">{Math.min(endIndex, totalUsers)}</span> de <span className="font-semibold">{totalUsers}</span> usuarios
</span>
<select
value={itemsPerPage}
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
className="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-navy-500 focus:border-navy-500"
>
<option value={5}>5 por página</option>
<option value={10}>10 por página</option>
<option value={15}>15 por página</option>
<option value={20}>20 por página</option>
</select>
{/* Cards para pantallas pequeñas */}
<div className="lg:hidden">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<span className="text-gray-500 text-lg">Cargando usuarios...</span>
</div>
</div>
) : error ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="text-red-600 text-lg">Error: {error}</span>
</div>
</div>
) : currentUsers.length > 0 ? (
<div className="space-y-4">
{currentUsers.map((user, idx) => (
<div key={user.id} className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-[1.02] fade-in-up-users" style={{ animationDelay: `${0.05 * idx}s` }}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-3 flex-1">
<div className="flex-shrink-0">
{user.profile_picture ? (
<img
className="h-12 w-12 rounded-full object-cover"
src={user.profile_picture}
alt="Avatar"
/>
) : (
<div className="h-12 w-12 rounded-full bg-gray-300 flex items-center justify-center">
<svg className="h-6 w-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 truncate"
dangerouslySetInnerHTML={{
__html: highlightText(user.username, debouncedSearchTerm)
}}
/>
<p className="text-xs text-gray-500 truncate"
dangerouslySetInnerHTML={{
__html: highlightText(user.email, debouncedSearchTerm)
}}
/>
<div className="text-xs text-gray-500 mt-1">ID: {user.id}</div>
</div>
</div>
<button
onClick={() => { setShowDeleteModal(true); setUserToDelete(user); }}
disabled={user.username === localStorage.getItem('username')}
className={`inline-flex items-center px-3 py-2 border border-red-300 shadow-sm text-xs font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 ${user.username === localStorage.getItem('username') ? 'opacity-50 cursor-not-allowed' : ''}`}
title={user.username === localStorage.getItem('username') ? 'No puedes eliminar tu propia cuenta' : 'Eliminar usuario'}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
<div className="grid grid-cols-2 gap-4 text-xs">
<div>
<span className="font-medium text-gray-500">Estado:</span>
<div className="mt-1 flex flex-wrap gap-1">
{user.is_active !== false ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Activo
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Inactivo
</span>
)}
{(user.is_staff || user.is_superuser) && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1z" clipRule="evenodd" />
</svg>
Admin
</span>
)}
</div>
</div>
<div>
<span className="font-medium text-gray-500">Nombre:</span>
<p className="text-gray-900 mt-1"
dangerouslySetInnerHTML={{
__html: user.first_name || user.last_name ?
highlightText(`${user.first_name} ${user.last_name}`.trim(), debouncedSearchTerm) :
'<span class="text-gray-400 italic">Sin nombre</span>'
}}
/>
</div>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12">
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay usuarios</h3>
<p className="text-gray-500 text-center">Aún no tienes usuarios registrados.</p>
</div>
)}
</div>
{totalPages > 1 && (
{/* Paginación mejorada */}
{totalPages > 1 && (
<div className="mt-8 border-t border-gray-200 pt-6">
<div className="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0">
<div className="text-sm text-gray-700">
Mostrando <span className="font-medium">{((currentPage - 1) * itemsPerPage) + 1}</span> a{' '}
<span className="font-medium">
{Math.min(currentPage * itemsPerPage, filteredUsers.length)}
</span> de{' '}
<span className="font-medium">{filteredUsers.length}</span> usuarios
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handlePageChange(currentPage - 1)}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-3 py-2 rounded-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"
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
</svg>
Anterior
</button>
<div className="hidden sm:flex space-x-1">
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const isCurrentPage = page === currentPage;
const isNearCurrentPage = Math.abs(page - currentPage) <= 2;
const isFirstOrLast = page === 1 || page === totalPages;
if (totalPages <= 7 || isNearCurrentPage || isFirstOrLast) {
return (
<button
key={page}
onClick={() => handlePageChange(page)}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md transition-colors ${isCurrentPage
? 'z-10 bg-navy-600 border-navy-600 text-white'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{page}
</button>
);
} else if (page === currentPage - 3 || page === currentPage + 3) {
return (
<span key={page} className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
...
</span>
);
}
return null;
})}
<div className="flex space-x-1">
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
currentPage === page
? 'bg-blue-600 text-white shadow-lg transform scale-105'
: 'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 hover:transform hover:scale-105'
}`}
>
{page}
</button>
))}
</div>
<div className="sm:hidden flex items-center space-x-2">
<span className="text-sm text-gray-700">
Página {currentPage} de {totalPages}
</span>
</div>
<button
onClick={() => handlePageChange(currentPage + 1)}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="relative inline-flex items-center px-3 py-2 rounded-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"
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
>
Siguiente
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -824,7 +927,7 @@ export default function Users() {
</svg>
</button>
</div>
)}
</div>
</div>
)}
</div>
@@ -890,7 +993,7 @@ export default function Users() {
<div className="mt-6">
<button
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@@ -935,7 +1038,7 @@ export default function Users() {
value={form.username}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="nombre_usuario"
/>
</div>
@@ -950,7 +1053,7 @@ export default function Users() {
value={form.email}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="usuario@ejemplo.com"
/>
</div>
@@ -986,7 +1089,7 @@ export default function Users() {
name="first_name"
value={form.first_name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="Nombre"
/>
</div>
@@ -1000,7 +1103,7 @@ export default function Users() {
name="last_name"
value={form.last_name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="Apellido"
/>
</div>
@@ -1015,7 +1118,7 @@ export default function Users() {
value={form.password}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="Contraseña del usuario"
/>
</div>
@@ -1026,14 +1129,14 @@ export default function Users() {
type="button"
onClick={handleCancel}
disabled={submitting}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50"
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50"
>
Cancelar
</button>
<button
type="submit"
disabled={submitting}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center"
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 flex items-center"
>
{submitting && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
@@ -1076,7 +1179,7 @@ export default function Users() {
value={form.username}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="nombre_usuario"
/>
</div>
@@ -1091,7 +1194,7 @@ export default function Users() {
value={form.email}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="usuario@ejemplo.com"
/>
</div>
@@ -1105,7 +1208,7 @@ export default function Users() {
name="first_name"
value={form.first_name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="Nombre"
/>
</div>
@@ -1119,7 +1222,7 @@ export default function Users() {
name="last_name"
value={form.last_name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="Apellido"
/>
</div>
@@ -1133,7 +1236,7 @@ export default function Users() {
name="password"
value={form.password}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-colors"
placeholder="Dejar vacío para mantener actual"
/>
<p className="mt-1 text-xs text-gray-500">
@@ -1147,14 +1250,14 @@ export default function Users() {
type="button"
onClick={handleCancel}
disabled={submitting}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50"
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50"
>
Cancelar
</button>
<button
type="submit"
disabled={submitting}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center"
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 flex items-center"
>
{submitting && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
@@ -1219,7 +1322,7 @@ export default function Users() {
<button
onClick={handleCancel}
disabled={submitting}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50"
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50"
>
Cancelar
</button>
@@ -1238,6 +1341,7 @@ export default function Users() {
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -16,6 +16,7 @@ export default function Vucem() {
usuario: '',
password: '',
patente: '',
efirma: '',
key: null,
cer: null,
is_importador: false,
@@ -84,36 +85,63 @@ export default function Vucem() {
// Reset page si cambia el filtro
useEffect(() => { setPage(1); }, [filterUsuario, filterPatente]);
// Cuando se selecciona un registro para editar, poblar el formulario con sus datos
useEffect(() => {
if (editVucem) {
setForm({
usuario: editVucem.usuario || '',
password: '', // No se rellena por seguridad
patente: editVucem.patente || '',
efirma: editVucem.efirma || '',
key: null, // No se rellena, solo se sube si el usuario selecciona
cer: null, // No se rellena, solo se sube si el usuario selecciona
is_importador: !!editVucem.is_importador,
acusecove: !!editVucem.acusecove,
acuseedocument: !!editVucem.acuseedocument,
is_active: !!editVucem.is_active,
});
}
}, [editVucem]);
// Table y header estilo Users.jsx
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header Mejorado igual que Users.jsx */}
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center gap-6">
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
VUCEM
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">{vucemList.length}</span>
<span className="inline-block bg-lime-200 text-lime-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in" title="Activos">
Activos: {vucemList.filter(v => v.is_active).length}
</span>
</h1>
<p className="text-lg text-blue-700/80 font-medium">Gestiona y supervisa los accesos y certificados VUCEM registrados en el sistema.</p>
</div>
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
{/* Header modernizado con gradientes azules */}
<div className="mb-8 relative overflow-hidden rounded-2xl shadow-xl bg-gradient-to-br from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-blue-900/30"></div>
<div className="absolute -top-24 -right-24 w-48 h-48 bg-white/10 rounded-full blur-xl"></div>
<div className="absolute -bottom-12 -left-12 w-32 h-32 bg-white/5 rounded-full blur-lg"></div>
<div className="relative flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6">
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-2xl p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-white tracking-tight">
Credenciales VU
</h1>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-white/20 text-white backdrop-blur-sm animate-fade-in">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 0a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1V8z" clipRule="evenodd" />
</svg>
{vucemList.length} Total
</span>
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-green-500/20 text-green-100 backdrop-blur-sm animate-fade-in" style={{ animationDelay: '0.1s' }}>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
{vucemList.filter(v => v.is_active).length} Activos
</span>
</div>
</div>
<p className="text-blue-100 text-sm sm:text-base font-medium">
Gestiona certificados digitales, credenciales y configuraciones VUCEM del sistema aduanero
</p>
</div>
</div>
</div>
<style>{`
@@ -131,190 +159,453 @@ export default function Vucem() {
.animate-fade-in {
animation: fade-in 0.7s ease;
}
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in-up-users {
animation: fade-in-up 0.4s ease forwards;
opacity: 0;
}
`}</style>
{/* Filtros y botón Crear VUCEM igual que Users.jsx */}
<div className="bg-white shadow rounded-lg mb-6">
<div className="px-6 py-4 border-b border-gray-200 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex-1 min-w-0 max-w-md">
<div className="relative bg-gray-50 rounded-lg border border-gray-200 shadow-sm">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z" />
</svg>
{/* Controles de búsqueda y filtros mejorados */}
<div className="bg-white shadow-lg rounded-xl border border-gray-100 mb-6 overflow-hidden">
<div className="bg-gradient-to-r from-gray-50 to-blue-50 px-4 sm:px-6 py-4 border-b border-gray-200">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
{/* Búsqueda principal */}
<div className="flex-1 max-w-md">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
className="block w-full pl-10 pr-10 py-3 border border-gray-300 rounded-lg bg-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 shadow-sm"
placeholder="Buscar por usuario o patente..."
value={filterUsuario}
onChange={e => setFilterUsuario(e.target.value)}
autoComplete="off"
/>
{filterUsuario && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
onClick={() => setFilterUsuario('')}
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-100"
title="Limpiar búsqueda"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
</div>
<input
type="text"
className="focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:bg-white block w-full pl-10 pr-10 py-3 sm:text-sm border-0 bg-transparent rounded-lg placeholder-gray-500 transition-all duration-200"
placeholder="Buscar por usuario o patente..."
value={filterUsuario}
onChange={e => setFilterUsuario(e.target.value)}
autoComplete="off"
/>
{filterUsuario && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
onClick={() => setFilterUsuario('')}
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-200"
title="Limpiar búsqueda"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</div>
{/* Información y botón crear */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
{filteredList.length !== vucemList.length && (
<div className="text-sm text-blue-700">
<span className="inline-flex items-center">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm8 0a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1V8z" />
</svg>
</button>
Mostrando {filteredList.length} de {vucemList.length} registros
{filterUsuario && <span className="ml-1">para "{filterUsuario}"</span>}
</span>
</div>
)}
<button
onClick={() => setShowCreateModal(true)}
type="button"
className="inline-flex items-center px-4 py-2.5 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Agregar Credencial
</button>
</div>
</div>
<div className="mt-4 sm:mt-0 sm:ml-4 flex gap-2">
<button
onClick={() => setShowCreateModal(true)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-indigo-600 to-indigo-700 hover:from-indigo-700 hover:to-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Crear VUCEM
</button>
</div>
</div>
{/* Tabla igual que Users.jsx */}
<div className="overflow-hidden">
{/* Vista responsiva: tabla para desktop, cards para mobile */}
{/* Tabla para pantallas grandes */}
<div className="hidden lg:block bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100">
<div style={{ minHeight: 'calc(8 * 56px)', maxHeight: 'calc(8 * 56px)', overflowY: 'auto', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden">
<thead className="bg-gray-50 sticky top-0 z-20">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gradient-to-r from-gray-50 to-blue-50 sticky top-0 z-20">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Usuario</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Patente</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Key</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cer</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Activo</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Usuario</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Patente</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Archivos</th>
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Estado</th>
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
{loading ? (
<tr>
<td colSpan={6} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<td colSpan={5} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-gray-500 text-lg">Cargando VUCEM...</span>
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<span className="text-gray-500 text-lg">Cargando registros VUCEM...</span>
</div>
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={6} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<td colSpan={5} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-danger-600 text-lg">Error: {error}</span>
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="text-red-600 text-lg">Error: {error}</span>
</div>
</div>
</td>
</tr>
) : paginatedList.length > 0 ? (
paginatedList.map((vucem, idx) => (
<tr
key={vucem.id}
className={
`transition-all duration-300 hover:scale-[1.025] hover:shadow-lg hover:bg-indigo-50 fade-in-up-users` +
(idx % 2 === 0 ? ' bg-white' : ' bg-gray-50')
}
style={{ animationDelay: `${0.05 * idx}s` }}
>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{vucem.usuario}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{vucem.patente}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{vucem.key ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Archivo cargado
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-400">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Sin archivo
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{vucem.cer ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Archivo cargado
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-400">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Sin archivo
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
{vucem.is_active ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Activo
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Inactivo
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<button onClick={() => { setEditVucem(vucem); setShowEditModal(true); }} className="text-indigo-600 hover:underline font-semibold mr-3">Editar</button>
<button onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }} className="text-red-600 hover:underline font-semibold">Eliminar</button>
</td>
</tr>
))
<>
{paginatedList.map((vucem, idx) => (
<tr
key={vucem.id}
className={
`transition-all duration-300 hover:scale-[1.015] hover:shadow-md hover:bg-blue-50 fade-in-up-users` +
(idx % 2 === 0 ? ' bg-white' : ' bg-gray-50')
}
style={{ animationDelay: `${0.05 * idx}s` }}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{vucem.usuario}</div>
<div className="text-sm text-gray-500">ID: {vucem.id}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{vucem.patente}</div>
<div className="text-sm text-gray-500">e.firma: {vucem.efirma}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col gap-1">
{vucem.key ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Key
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Sin Key
</span>
)}
{vucem.cer ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Cer
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Sin Cer
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<div className="flex flex-col items-center gap-1">
{vucem.is_active ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Activo
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Inactivo
</span>
)}
{vucem.is_importador && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1z" clipRule="evenodd" />
</svg>
Importador
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<div className="flex justify-center space-x-2">
<button
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
className="inline-flex items-center px-3 py-1.5 border border-blue-300 shadow-sm text-xs font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Editar
</button>
<button
onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }}
className="inline-flex items-center px-3 py-1.5 border border-red-300 shadow-sm text-xs font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 transform hover:scale-105"
>
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Eliminar
</button>
</div>
</td>
</tr>
))}
{/* Rellenar con filas vacías si hay menos de 8 */}
{paginatedList.length < 8 && !loading && !error && Array.from({ length: 8 - paginatedList.length }).map((_, idx) => (
<tr key={`empty-${idx}`}>
<td className="px-6 py-4 whitespace-nowrap" colSpan={5}>&nbsp;</td>
</tr>
))}
</>
) : (
<tr>
<td colSpan={6} className="text-center py-8 text-gray-500">No hay registros</td>
<td colSpan={5} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay registros VUCEM</h3>
<p className="text-gray-500">Comienza creando tu primer registro VUCEM.</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Paginación igual que Users.jsx */}
{filteredList.length > 0 && (
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
<div className="flex items-center mb-4 sm:mb-0">
<span className="text-sm text-gray-700">
Mostrando <span className="font-semibold">{filteredList.length}</span> de <span className="font-semibold">{vucemList.length}</span> registros
</span>
{/* Cards para pantallas pequeñas */}
<div className="lg:hidden">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<span className="text-gray-500 text-lg">Cargando registros VUCEM...</span>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className={`px-3 py-1 rounded-lg border text-base font-semibold transition-colors ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-gray-700 border-gray-300 hover:bg-indigo-50 hover:text-indigo-700'}`}
>
Anterior
</button>
<span className="mx-2 text-base font-medium text-gray-700">Página {page} de {totalPages}</span>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className={`px-3 py-1 rounded-lg border text-base font-semibold transition-colors ${page === totalPages ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-gray-700 border-gray-300 hover:bg-indigo-50 hover:text-indigo-700'}`}
>
Siguiente
</button>
) : error ? (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="text-red-600 text-lg">Error: {error}</span>
</div>
</div>
) : paginatedList.length > 0 ? (
<div className="space-y-4">
{paginatedList.map((vucem, idx) => (
<div key={vucem.id} className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all duration-300 hover:scale-[1.02] fade-in-up-users" style={{ animationDelay: `${0.05 * idx}s` }}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center space-x-3 flex-1">
<div className="flex-shrink-0 bg-blue-100 rounded-full p-2">
<svg className="h-5 w-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 truncate">{vucem.usuario}</h3>
<p className="text-xs text-gray-500 truncate">Patente: {vucem.patente}</p>
<div className="text-xs text-gray-500 mt-1">ID: {vucem.id}</div>
</div>
</div>
<div className="flex flex-col gap-2">
<button
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
className="inline-flex items-center px-3 py-1.5 border border-blue-300 shadow-sm text-xs font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
>
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Editar
</button>
<button
onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }}
className="inline-flex items-center px-3 py-1.5 border border-red-300 shadow-sm text-xs font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200"
>
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Eliminar
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4 text-xs">
<div>
<span className="font-medium text-gray-500">e.firma:</span>
<p className="text-gray-900 mt-1 truncate">{vucem.efirma}</p>
</div>
<div>
<span className="font-medium text-gray-500">Estado:</span>
<div className="mt-1 flex flex-wrap gap-1">
{vucem.is_active ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
Activo
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Inactivo
</span>
)}
{vucem.is_importador && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1z" clipRule="evenodd" />
</svg>
Importador
</span>
)}
</div>
</div>
<div className="col-span-2">
<span className="font-medium text-gray-500">Archivos:</span>
<div className="mt-1 flex flex-wrap gap-1">
{vucem.key ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Key
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Sin Key
</span>
)}
{vucem.cer ? (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 7.293a1 1 0 00-1.414 0L9 13.586l-2.293-2.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l7-7a1 1 0 000-1.414z" clipRule="evenodd" />
</svg>
Cer
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
<svg className="w-2 h-2 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
Sin Cer
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12">
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay registros VUCEM</h3>
<p className="text-gray-500 text-center">Comienza creando tu primer registro VUCEM.</p>
</div>
)}
</div>
</div>
{/* Paginación mejorada */}
{totalPages > 1 && (
<div className="mt-8 border-t border-gray-200 pt-6">
<div className="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0">
<div className="text-sm text-gray-700">
Mostrando <span className="font-medium">{((page - 1) * pageSize) + 1}</span> a{' '}
<span className="font-medium">
{Math.min(page * pageSize, filteredList.length)}
</span> de{' '}
<span className="font-medium">{filteredList.length}</span> registros
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
</svg>
Anterior
</button>
<div className="flex space-x-1">
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
className={`px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
page === pageNum
? 'bg-blue-600 text-white shadow-lg transform scale-105'
: 'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 hover:transform hover:scale-105'
}`}
>
{pageNum}
</button>
))}
</div>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
>
Siguiente
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
</div>
)}
</div>
{/* Modal de creación */}
{showCreateModal && (
@@ -332,6 +623,7 @@ export default function Vucem() {
formData.append('usuario', form.usuario);
formData.append('password', form.password);
formData.append('patente', form.patente);
formData.append('efirma', form.efirma);
if (form.key) formData.append('key', form.key);
if (form.cer) formData.append('cer', form.cer);
formData.append('is_importador', form.is_importador);
@@ -365,6 +657,10 @@ export default function Vucem() {
<label className="block text-sm font-medium text-gray-700 mb-1">Patente *</label>
<input name="patente" value={form.patente} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="patente" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">e.firma *</label>
<input name="efirma" value={form.efirma} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="e.firma" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Key (.key)</label>
<input name="key" type="file" accept=".key" onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm" />
@@ -393,8 +689,8 @@ export default function Vucem() {
</div>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50">Cancelar</button>
<button type="submit" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center">Crear</button>
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50">Cancelar</button>
<button type="submit" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 flex items-center">Crear</button>
</div>
</form>
</div>
@@ -418,6 +714,7 @@ export default function Vucem() {
formData.append('usuario', form.usuario);
if (form.password) formData.append('password', form.password);
formData.append('patente', form.patente);
formData.append('efirma', form.efirma);
if (form.key) formData.append('key', form.key);
if (form.cer) formData.append('cer', form.cer);
formData.append('is_importador', form.is_importador);
@@ -451,6 +748,10 @@ export default function Vucem() {
<label className="block text-sm font-medium text-gray-700 mb-1">Patente *</label>
<input name="patente" value={form.patente} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="patente" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">e.firma *</label>
<input name="efirma" value={form.efirma} onChange={handleInputChange} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" placeholder="e.firma" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Key (.key)</label>
<input name="key" type="file" accept=".key" onChange={handleInputChange} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm" />
@@ -479,8 +780,8 @@ export default function Vucem() {
</div>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50">Cancelar</button>
<button type="submit" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center">Actualizar</button>
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50">Cancelar</button>
<button type="submit" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 flex items-center">Actualizar</button>
</div>
</form>
</div>
@@ -505,7 +806,7 @@ export default function Vucem() {
<p className="text-sm text-red-600">Esta acción no se puede deshacer.</p>
</div>
<div className="flex justify-center space-x-3 pt-4">
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50">Cancelar</button>
<button type="button" onClick={closeModals} className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50">Cancelar</button>
<button type="button" onClick={async () => {
if (!deleteVucem) return;
try {