Se mejoro estetica y estandarizaron estilos
This commit is contained in:
4
.env
4
.env
@@ -1,4 +1,4 @@
|
|||||||
VITE_DEBUG_MODE=false
|
VITE_DEBUG_MODE=false
|
||||||
|
|
||||||
VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1
|
VITE_EFC_API_URL=http://192.168.1.195:8000/api/v1
|
||||||
VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservice/api/v1
|
VITE_EFC_MICROSERVICE_URL=http://192.168.1.195:8001/api/v1
|
||||||
|
|||||||
34
src/components/ResponsiveSidebar.jsx
Normal file
34
src/components/ResponsiveSidebar.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
src/components/SIDEBAR_README.md
Normal file
115
src/components/SIDEBAR_README.md
Normal 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! 📱💻
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useUser } from '../context/UserContext';
|
import { useUser } from '../context/UserContext';
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar({ isMobileOpen, onMobileClose }) {
|
||||||
// Leer si el usuario es importador desde localStorage
|
// Leer si el usuario es importador desde localStorage
|
||||||
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
|
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
|
||||||
// Leer grupos del usuario desde localStorage
|
// 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);
|
const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5);
|
||||||
// Leer DEBUG_MODE desde variables de entorno
|
// Leer DEBUG_MODE desde variables de entorno
|
||||||
const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true';
|
const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true';
|
||||||
|
|
||||||
|
// Estados para responsividad
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
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 location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user: currentUser, loading } = useUser();
|
const { user: currentUser, loading } = useUser();
|
||||||
@@ -30,6 +38,23 @@ export default function Sidebar() {
|
|||||||
navigate('/login');
|
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
|
// El usuario y loading ahora vienen del contexto global
|
||||||
|
|
||||||
// Definir todas las secciones
|
// Definir todas las secciones
|
||||||
@@ -196,35 +221,79 @@ export default function Sidebar() {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Botón flotante para abrir menú en móvil - solo cuando se usa standalone */}
|
||||||
<div className="p-4 border-b border-slate-700 flex-shrink-0">
|
{!mobileOpen && isMobileOpen === undefined && (
|
||||||
<div className="flex items-center justify-between">
|
<button
|
||||||
{!isCollapsed && (
|
onClick={handleMobileOpen}
|
||||||
<div className="flex items-center">
|
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"
|
||||||
{/* Logo de la organización */}
|
aria-label="Abrir menú"
|
||||||
<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">
|
<svg className="w-5 h-5" 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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
</svg>
|
</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>
|
</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>
|
</div>
|
||||||
)}
|
</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 */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 p-3 space-y-4 overflow-y-auto overflow-x-hidden">
|
<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>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,123 +87,124 @@ export default function Admin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header + Estado del Sistema alineados horizontalmente */}
|
{/* 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 */}
|
{/* 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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards',
|
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">
|
<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-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
<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">
|
||||||
Panel de Administración
|
<span>Panel de Administración</span>
|
||||||
{services && (
|
{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
|
{services.en_espera} en espera
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h1>
|
</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'
|
{typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'
|
||||||
? 'Dashboard principal para gestión de Expediente electrónico'
|
? 'Dashboard principal para gestión de Expediente electrónico'
|
||||||
: 'Dashboard principal para gestión de agencia aduanal'}
|
: 'Dashboard principal para gestión de agencia aduanal'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Efecto decorativo de fondo */}
|
{/* Efectos decorativos de fondo modernos */}
|
||||||
<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">
|
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
|
||||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
</div>
|
||||||
<defs>
|
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
|
||||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
|
||||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
</div>
|
||||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
{/* Partículas flotantes */}
|
||||||
</linearGradient>
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
</defs>
|
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
|
||||||
</svg>
|
<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 */}
|
{/* Animación personalizada para el icono y contador */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes bounce-slow {
|
@keyframes bounce-slow {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
50% { transform: translateY(-8px); }
|
50% { transform: translateY(-8px) scale(1.05); }
|
||||||
}
|
}
|
||||||
.animate-bounce-slow {
|
.animate-bounce-slow {
|
||||||
animation: bounce-slow 2.2s infinite;
|
animation: bounce-slow 3s infinite;
|
||||||
}
|
}
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from { opacity: 0; transform: scale(0.9); }
|
from { opacity: 0; transform: scale(0.9) translateY(10px); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
.animate-fade-in {
|
.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>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
{/* Estado del Sistema card a la derecha */}
|
{/* 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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards',
|
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="flex items-center gap-3 mb-4">
|
||||||
<div className="bg-green-100 rounded-full p-3 shadow-md animate-bounce-slow">
|
<div className="bg-gradient-to-br from-emerald-500 to-green-600 rounded-full p-3 shadow-lg animate-bounce-slow">
|
||||||
<svg className="h-7 w-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</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>
|
||||||
<div className="space-y-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<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">API Backend</span>
|
<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-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1">
|
<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>
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||||
Conectado
|
Conectado
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<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">API Servicios</span>
|
<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-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1">
|
<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>
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||||
Conectado
|
Conectado
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<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">Última Actualización</span>
|
<span className="text-gray-700 font-medium text-sm sm:text-base">Ú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">
|
<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
|
Hace 2 min
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Efecto decorativo de fondo */}
|
{/* Efecto decorativo de fondo modernizado */}
|
||||||
<div className="absolute -top-8 -right-8 opacity-20 pointer-events-none select-none">
|
<div className="absolute -top-8 -right-8 opacity-10 pointer-events-none select-none">
|
||||||
<svg width="80" height="80" viewBox="0 0 120 120" fill="none">
|
<div className="w-24 h-24 bg-gradient-to-br from-green-400 to-blue-500 rounded-full blur-xl"></div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Animación personalizada para el icono */}
|
{/* Animación personalizada para el icono */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes bounce-slow {
|
@keyframes bounce-slow {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
50% { transform: translateY(-8px); }
|
50% { transform: translateY(-8px) scale(1.05); }
|
||||||
}
|
}
|
||||||
.animate-bounce-slow {
|
.animate-bounce-slow {
|
||||||
animation: bounce-slow 2.2s infinite;
|
animation: bounce-slow 3s infinite;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,170 +212,290 @@ export default function Admin() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards con datos de endpoints */}
|
{/* 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 */}
|
{/* 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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards',
|
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<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="flex-shrink-0">
|
<div className="relative z-10">
|
||||||
<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">
|
<div className="flex items-center">
|
||||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex-shrink-0">
|
||||||
<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" />
|
<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>
|
<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>
|
|
||||||
<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>
|
||||||
</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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.35s forwards',
|
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.35s forwards',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<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="flex-shrink-0">
|
<div className="relative z-10">
|
||||||
<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">
|
<div className="flex items-center">
|
||||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex-shrink-0">
|
||||||
<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" />
|
<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>
|
<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>
|
|
||||||
<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>
|
||||||
</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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.45s forwards',
|
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.45s forwards',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<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="flex-shrink-0">
|
<div className="relative z-10">
|
||||||
<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">
|
<div className="flex items-center">
|
||||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex-shrink-0">
|
||||||
<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" />
|
<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>
|
<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>
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Descargas */}
|
{/* 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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.55s forwards',
|
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.55s forwards',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<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="flex-shrink-0">
|
<div className="relative z-10">
|
||||||
<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">
|
<div className="flex items-center">
|
||||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex-shrink-0">
|
||||||
<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" />
|
<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>
|
<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>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Análisis de actividad de usuario */}
|
{/* Análisis de actividad de usuario */}
|
||||||
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && !isGroup35 && (
|
{!(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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.65s forwards',
|
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>
|
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/3 to-purple-500/3"></div>
|
||||||
{loading ? (
|
<div className="relative z-10">
|
||||||
<div className="text-gray-500">Cargando...</div>
|
<div className="flex items-center gap-3 mb-4 sm:mb-6">
|
||||||
) : error ? (
|
<div className="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full p-3 shadow-lg">
|
||||||
<div className="text-danger-600">{error}</div>
|
<svg className="h-6 w-6 sm:h-7 sm:w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
) : userActivity ? (
|
<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" />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
</svg>
|
||||||
<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>
|
</div>
|
||||||
|
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">Actividad de Usuarios</h3>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tabla de últimos documentos */}
|
{/* 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={{
|
style={{
|
||||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.75s forwards',
|
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>
|
<div className="absolute inset-0 bg-gradient-to-br from-slate-500/2 to-gray-500/3"></div>
|
||||||
{loading ? (
|
<div className="relative z-10">
|
||||||
<div className="text-gray-500">Cargando...</div>
|
<div className="flex items-center gap-3 mb-4 sm:mb-6">
|
||||||
) : error ? (
|
<div className="bg-gradient-to-br from-slate-600 to-gray-700 rounded-full p-3 shadow-lg">
|
||||||
<div className="text-danger-600">{error}</div>
|
<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" />
|
||||||
<div className="overflow-x-auto">
|
</svg>
|
||||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
</div>
|
||||||
<thead className="bg-gray-50">
|
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">Últimos documentos agregados</h3>
|
||||||
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
|
||||||
|
|
||||||
return (
|
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 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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header mejorado y decorativo */}
|
{/* Header mejorado y decorativo */}
|
||||||
<div className={
|
<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' : '')
|
(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}>
|
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">
|
<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-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
<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
|
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>
|
</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>
|
</div>
|
||||||
{/* Efecto decorativo de fondo */}
|
{/* 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">
|
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
|
||||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
||||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
<stop stopColor="#1e40af" stopOpacity="0.15" />
|
||||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
<stop offset="1" stopColor="#1e3a8a" stopOpacity="0.10" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -324,37 +324,37 @@ export default function Documents() {
|
|||||||
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
(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}>
|
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">
|
<div className="overflow-x-auto" id="tabla-documentos">
|
||||||
{/* Header de Documentos Relacionados arriba de los filtros */}
|
{/* Header de Documentos Relacionados arriba de los filtros */}
|
||||||
<div className="px-8 pt-8 pb-2 border-b border-gray-200">
|
<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-2xl font-extrabold text-blue-800 tracking-tight mb-1">
|
<h2 className="text-xl sm:text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
|
||||||
Todos los Documentos
|
Todos los Documentos
|
||||||
</h2>
|
</h2>
|
||||||
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
|
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
|
||||||
</div>
|
</div>
|
||||||
{/* Filtros de query parameters */}
|
{/* 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 */}
|
{/* 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 */}
|
{/* 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>
|
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento Número</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={pedimentoNumeroFilter}
|
value={pedimentoNumeroFilter}
|
||||||
onChange={e => setPedimentoNumeroFilter(e.target.value)}
|
onChange={e => setPedimentoNumeroFilter(e.target.value)}
|
||||||
placeholder="Buscar por número de pedimento..."
|
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"
|
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>
|
||||||
{/* Extensión */}
|
{/* 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>
|
<label className="text-xs font-semibold text-gray-700 mb-1">Extensión</label>
|
||||||
<select
|
<select
|
||||||
value={extensionFilter}
|
value={extensionFilter}
|
||||||
onChange={e => setExtensionFilter(e.target.value)}
|
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="">Todas</option>
|
||||||
<option value="pdf">PDF</option>
|
<option value="pdf">PDF</option>
|
||||||
@@ -372,12 +372,12 @@ export default function Documents() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{/* Tipo de documento */}
|
{/* 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>
|
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de documento</label>
|
||||||
<select
|
<select
|
||||||
value={documentTypeFilter}
|
value={documentTypeFilter}
|
||||||
onChange={e => setDocumentTypeFilter(e.target.value)}
|
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="">Todos</option>
|
||||||
<option value="1">Pedimento Partida</option>
|
<option value="1">Pedimento Partida</option>
|
||||||
@@ -389,13 +389,13 @@ export default function Documents() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{/* Fecha de creación */}
|
{/* 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>
|
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de creación</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={createdAtFilter}
|
value={createdAtFilter}
|
||||||
onChange={e => setCreatedAtFilter(e.target.value)}
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,86 +403,218 @@ export default function Documents() {
|
|||||||
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
||||||
{/* Botones de descarga */}
|
{/* Botones de descarga */}
|
||||||
{currentDocuments.length > 0 && (
|
{currentDocuments.length > 0 && (
|
||||||
<div className="flex space-x-3 mb-2">
|
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadAll}
|
onClick={handleDownloadAll}
|
||||||
disabled={currentDocuments.length === 0}
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Descargar todos
|
<span className="hidden sm:inline">Descargar todos</span>
|
||||||
|
<span className="sm:hidden">Todos</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadSelected}
|
onClick={handleDownloadSelected}
|
||||||
disabled={selectedDocs.length === 0}
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Descargar seleccionados ({selectedDocs.length})
|
<span className="hidden sm:inline">Descargar seleccionados ({selectedDocs.length})</span>
|
||||||
|
<span className="sm:hidden">Seleccionados ({selectedDocs.length})</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: currentDocuments.length > 6 ? 'auto' : 'hidden', position: 'relative' }}>
|
</div>
|
||||||
<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">
|
{/* Vista responsiva: tabla para desktop, cards para mobile */}
|
||||||
<tr>
|
{/* Tabla para pantallas grandes */}
|
||||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">
|
<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}> </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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={allSelected}
|
checked={allSelected}
|
||||||
ref={el => { if (el) el.indeterminate = someSelected; }}
|
ref={el => { if (el) el.indeterminate = someSelected; }}
|
||||||
onChange={handleSelectAll}
|
onChange={handleSelectAll}
|
||||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
|
||||||
/>
|
/>
|
||||||
</th>
|
<span className="text-sm font-medium text-gray-700">Seleccionar todos</span>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Pedimento</th>
|
</label>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Archivo</th>
|
<span className="text-sm text-gray-500">{selectedDocs.length} seleccionados</span>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
|
</div>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tamaño</th>
|
{currentDocuments.map(doc => (
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Extensión</th>
|
<div key={doc.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
|
||||||
<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>
|
<div className="flex items-start space-x-3 mb-3">
|
||||||
</tr>
|
<input
|
||||||
</thead>
|
type="checkbox"
|
||||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
checked={selectedDocs.includes(doc.id)}
|
||||||
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
|
onChange={() => handleSelectOne(doc.id)}
|
||||||
{loading ? (
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-0.5 flex-shrink-0"
|
||||||
<tr>
|
/>
|
||||||
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
<div className="flex-1 min-w-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 }}>
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
<span className="text-gray-500 text-lg">Cargando documentos...</span>
|
Pedimento: {doc.pedimento_numero}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-1 break-all">
|
||||||
|
{doc.archivo ? doc.archivo.split('/').pop() : ''}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
) : error ? (
|
<div className="grid grid-cols-2 gap-4 text-xs mb-4">
|
||||||
<tr>
|
<div>
|
||||||
<td colSpan={10} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
<span className="font-medium text-gray-500">Tipo:</span>
|
||||||
<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 }}>
|
<p className="text-gray-900 mt-1">{
|
||||||
<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">{
|
|
||||||
(() => {
|
(() => {
|
||||||
switch (String(doc.document_type)) {
|
switch (String(doc.document_type)) {
|
||||||
case '1': return 'Pedimento Partida';
|
case '1': return 'Pedimento Partida';
|
||||||
@@ -494,150 +626,142 @@ export default function Documents() {
|
|||||||
default: return doc.document_type || '';
|
default: return doc.document_type || '';
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}</td>
|
}</p>
|
||||||
<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}> </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>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div>
|
||||||
</tr>
|
<span className="font-medium text-gray-500">Tamaño:</span>
|
||||||
)}
|
<p className="text-gray-900 mt-1">{doc.size}</p>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botón de actualizar eliminado por solicitud */}
|
{/* Botón de actualizar eliminado por solicitud */}
|
||||||
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-hidden">
|
|
||||||
|
|
||||||
{/* Paginación con botones numerados y elipsis */}
|
{/* Paginación con botones numerados y elipsis */}
|
||||||
{totalDocuments > 0 && (
|
{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-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
||||||
{(() => {
|
<div className="flex flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
|
||||||
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
|
<div className="flex items-center gap-2">
|
||||||
const maxPagesToShow = 5;
|
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
||||||
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
|
<select
|
||||||
let endPage = startPage + maxPagesToShow - 1;
|
id="itemsPerPage"
|
||||||
if (endPage > totalPages) {
|
value={itemsPerPage}
|
||||||
endPage = totalPages;
|
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
|
||||||
startPage = Math.max(1, endPage - maxPagesToShow + 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"
|
||||||
}
|
>
|
||||||
const pageNumbers = [];
|
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
<option key={size} value={size}>{size}</option>
|
||||||
pageNumbers.push(i);
|
))}
|
||||||
}
|
</select>
|
||||||
return (
|
</div>
|
||||||
<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-1 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<button
|
||||||
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
type="button"
|
||||||
<select
|
onClick={e => handlePageChange(1, e)}
|
||||||
id="itemsPerPage"
|
disabled={currentPage === 1}
|
||||||
value={itemsPerPage}
|
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'}`}
|
||||||
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"
|
«
|
||||||
>
|
</button>
|
||||||
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
|
<button
|
||||||
<option key={size} value={size}>{size}</option>
|
type="button"
|
||||||
))}
|
onClick={e => handlePageChange(currentPage - 1, e)}
|
||||||
</select>
|
disabled={currentPage === 1}
|
||||||
</div>
|
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'}`}
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
>
|
||||||
|
‹
|
||||||
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={e => handlePageChange(1, e)}
|
key={num}
|
||||||
disabled={currentPage === 1}
|
onClick={e => handlePageChange(num, e)}
|
||||||
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-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>
|
||||||
<button
|
));
|
||||||
type="button"
|
})()}
|
||||||
onClick={e => handlePageChange(currentPage - 1, e)}
|
<button
|
||||||
disabled={currentPage === 1}
|
type="button"
|
||||||
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'}`}
|
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>
|
>
|
||||||
{pageNumbers.map(num => (
|
›
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
key={num}
|
type="button"
|
||||||
onClick={e => handlePageChange(num, e)}
|
onClick={e => handlePageChange(Math.ceil(totalDocuments / itemsPerPage), 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={currentPage >= Math.ceil(totalDocuments / itemsPerPage)}
|
||||||
disabled={num === currentPage}
|
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'}`}
|
||||||
>
|
>
|
||||||
{num}
|
»
|
||||||
</button>
|
</button>
|
||||||
))}
|
<span className="ml-3 text-xs text-gray-500">
|
||||||
<button
|
Página <span className="font-bold">{currentPage}</span> de <span className="font-bold">{Math.ceil(totalDocuments / itemsPerPage)}</span>
|
||||||
type="button"
|
</span>
|
||||||
onClick={e => handlePageChange(currentPage + 1, e)}
|
</div>
|
||||||
disabled={currentPage >= totalPages}
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
|
||||||
|
|
||||||
return (
|
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 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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header mejorado y decorativo */}
|
{/* Header mejorado y decorativo */}
|
||||||
<div className={
|
<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' : '')
|
(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}>
|
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">
|
<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-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
<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">
|
||||||
Expedientes
|
<span>Expedientes</span>
|
||||||
<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>
|
{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>
|
</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>
|
</div>
|
||||||
{/* Efecto decorativo de fondo */}
|
{/* Efectos decorativos de fondo modernos */}
|
||||||
<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">
|
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
|
||||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
</div>
|
||||||
<defs>
|
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
|
||||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
|
||||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
</div>
|
||||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
{/* Partículas flotantes */}
|
||||||
</linearGradient>
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
</defs>
|
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
|
||||||
</svg>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{/* Animación personalizada para el icono y contador */}
|
{/* Animación personalizada para el icono y contador */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes bounce-slow {
|
@keyframes bounce-slow {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
50% { transform: translateY(-8px); }
|
50% { transform: translateY(-8px) scale(1.05); }
|
||||||
}
|
}
|
||||||
.animate-bounce-slow {
|
.animate-bounce-slow {
|
||||||
animation: bounce-slow 2.2s infinite;
|
animation: bounce-slow 3s infinite;
|
||||||
}
|
}
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from { opacity: 0; transform: scale(0.9); }
|
from { opacity: 0; transform: scale(0.9) translateY(10px); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fade-in 0.7s ease;
|
animation: fade-in 0.8s ease-out;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div className={
|
<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' : '')
|
(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}>
|
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 */}
|
{/* Filtros avanzados */}
|
||||||
<div className="mb-4 flex flex-wrap gap-4 items-end">
|
<div className="mb-4 sm:mb-6">
|
||||||
{/* Search global */}
|
<h3 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">
|
||||||
<div className="flex flex-col">
|
<svg className="w-4 h-4 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Buscar</label>
|
<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" />
|
||||||
<input
|
</svg>
|
||||||
type="text"
|
Filtros de búsqueda
|
||||||
value={searchFilter}
|
</h3>
|
||||||
onChange={e => setSearchFilter(e.target.value)}
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
|
||||||
placeholder="Buscar pedimento, contribuyente, agente aduanal..."
|
{/* Search global */}
|
||||||
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 className="flex flex-col">
|
||||||
/>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Buscar</label>
|
||||||
</div>
|
<input
|
||||||
{/* Pedimento */}
|
type="text"
|
||||||
<div className="flex flex-col">
|
value={searchFilter}
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
|
onChange={e => setSearchFilter(e.target.value)}
|
||||||
<input
|
placeholder="Buscar pedimento, contribuyente..."
|
||||||
type="text"
|
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"
|
||||||
value={pedimentoFilter}
|
/>
|
||||||
onChange={e => setPedimentoFilter(e.target.value)}
|
</div>
|
||||||
placeholder="Buscar pedimento..."
|
{/* 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="flex flex-col">
|
||||||
/>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Pedimento</label>
|
||||||
</div>
|
<input
|
||||||
{/* Alerta */}
|
type="text"
|
||||||
<div className="flex flex-col">
|
value={pedimentoFilter}
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Alerta</label>
|
onChange={e => setPedimentoFilter(e.target.value)}
|
||||||
<select value={alertaFilter} onChange={e => setAlertaFilter(e.target.value)}
|
placeholder="Número de 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">
|
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">Sí</option>
|
</div>
|
||||||
<option value="false">No</option>
|
{/* Expediente */}
|
||||||
</select>
|
<div className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Expediente</label>
|
||||||
{/* Expediente */}
|
<select value={expedienteFilter} onChange={e => setExpedienteFilter(e.target.value)}
|
||||||
<div className="flex flex-col">
|
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">
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Expediente</label>
|
<option value="all">Todos</option>
|
||||||
<select value={expedienteFilter} onChange={e => setExpedienteFilter(e.target.value)}
|
<option value="true">Con expediente</option>
|
||||||
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="false">Sin expediente</option>
|
||||||
<option value="all">Todos</option>
|
</select>
|
||||||
<option value="true">Sí</option>
|
</div>
|
||||||
<option value="false">No</option>
|
{/* Contribuyente combobox */}
|
||||||
</select>
|
<div className="flex flex-col relative">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Contribuyente</label>
|
||||||
{/* Contribuyente combobox */}
|
<input
|
||||||
<div className="flex flex-col relative">
|
type="text"
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Contribuyente</label>
|
value={contribuyenteInput}
|
||||||
<input
|
onChange={e => {
|
||||||
type="text"
|
setContribuyenteInput(e.target.value);
|
||||||
value={contribuyenteInput}
|
setContribuyenteFilter('');
|
||||||
onChange={e => {
|
}}
|
||||||
setContribuyenteInput(e.target.value);
|
placeholder="Buscar o escribir..."
|
||||||
setContribuyenteFilter('');
|
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"
|
||||||
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"
|
{/* Dropdown de sugerencias */}
|
||||||
autoComplete="off"
|
{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">
|
||||||
{/* Dropdown de sugerencias */}
|
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
|
||||||
{contribuyenteInput && (
|
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
|
||||||
<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 ? (
|
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
|
||||||
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
|
<button
|
||||||
) : (
|
key={c}
|
||||||
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
|
type="button"
|
||||||
<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"
|
||||||
key={c}
|
onClick={() => {
|
||||||
type="button"
|
setContribuyenteFilter(c);
|
||||||
className="w-full text-left px-3 py-2 hover:bg-blue-50 text-sm"
|
setContribuyenteInput('');
|
||||||
onClick={() => {
|
}}
|
||||||
setContribuyenteFilter(c);
|
>
|
||||||
setContribuyenteInput('');
|
{c}
|
||||||
}}
|
</button>
|
||||||
>
|
))
|
||||||
{c}
|
)}
|
||||||
</button>
|
</div>
|
||||||
))
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
{/* CURP Apoderado */}
|
||||||
)}
|
<div className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">CURP Apoderado</label>
|
||||||
{/* CURP Apoderado */}
|
<input
|
||||||
<div className="flex flex-col">
|
type="text"
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">CURP Apoderado</label>
|
value={curpApoderadoFilter}
|
||||||
<input
|
onChange={e => setCurpApoderadoFilter(e.target.value)}
|
||||||
type="text"
|
placeholder="CURP del apoderado..."
|
||||||
value={curpApoderadoFilter}
|
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"
|
||||||
onChange={e => setCurpApoderadoFilter(e.target.value)}
|
/>
|
||||||
placeholder="CURP del apoderado..."
|
</div>
|
||||||
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"
|
{/* Fecha de pago */}
|
||||||
/>
|
<div className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Fecha de pago</label>
|
||||||
{/* Fecha de pago */}
|
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
|
||||||
<div className="flex flex-col">
|
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" />
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de pago</label>
|
</div>
|
||||||
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
|
{/* Patente */}
|
||||||
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 className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Patente</label>
|
||||||
{/* Patente */}
|
<input
|
||||||
<div className="flex flex-col">
|
type="text"
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Patente</label>
|
value={patenteFilter}
|
||||||
<input
|
onChange={e => setPatenteFilter(e.target.value)}
|
||||||
type="text"
|
placeholder="Patente..."
|
||||||
value={patenteFilter}
|
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"
|
||||||
onChange={e => setPatenteFilter(e.target.value)}
|
/>
|
||||||
placeholder="Patente..."
|
</div>
|
||||||
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"
|
{/* Aduana */}
|
||||||
/>
|
<div className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Aduana</label>
|
||||||
{/* Aduana */}
|
<input
|
||||||
<div className="flex flex-col">
|
type="text"
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Aduana</label>
|
value={aduanaFilter}
|
||||||
<input
|
onChange={e => setAduanaFilter(e.target.value)}
|
||||||
type="text"
|
placeholder="Aduana..."
|
||||||
value={aduanaFilter}
|
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"
|
||||||
onChange={e => setAduanaFilter(e.target.value)}
|
/>
|
||||||
placeholder="Aduana..."
|
</div>
|
||||||
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"
|
{/* Tipo de operación */}
|
||||||
/>
|
<div className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Tipo de operación</label>
|
||||||
{/* Tipo de operación */}
|
<input
|
||||||
<div className="flex flex-col">
|
type="text"
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de operación</label>
|
value={tipoOperacionFilter}
|
||||||
<input
|
onChange={e => setTipoOperacionFilter(e.target.value)}
|
||||||
type="text"
|
placeholder="ID tipo operación..."
|
||||||
value={tipoOperacionFilter}
|
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"
|
||||||
onChange={e => setTipoOperacionFilter(e.target.value)}
|
/>
|
||||||
placeholder="ID tipo operación..."
|
</div>
|
||||||
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"
|
{/* Clave pedimento */}
|
||||||
/>
|
<div className="flex flex-col">
|
||||||
</div>
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Clave pedimento</label>
|
||||||
{/* Clave pedimento */}
|
<input
|
||||||
<div className="flex flex-col">
|
type="text"
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Clave pedimento</label>
|
value={clavePedimentoFilter}
|
||||||
<input
|
onChange={e => setClavePedimentoFilter(e.target.value)}
|
||||||
type="text"
|
placeholder="Clave pedimento..."
|
||||||
value={clavePedimentoFilter}
|
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"
|
||||||
onChange={e => setClavePedimentoFilter(e.target.value)}
|
/>
|
||||||
placeholder="Clave pedimento..."
|
</div>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
|
<div className="flex items-center gap-3">
|
||||||
🔄 Actualización automática cada 30 segundos
|
<span className="inline-flex items-center text-xs text-blue-600 bg-blue-50 px-3 py-2 rounded-full font-medium">
|
||||||
</span>
|
<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
|
<button
|
||||||
onClick={refetch}
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Actualizar Ahora
|
Actualizar Ahora
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{success && (
|
{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">
|
<div className="flex">
|
||||||
<svg className="h-5 w-5 text-green-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="flex-shrink-0">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
<svg className="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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>
|
||||||
<p className="text-green-800">{success}</p>
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm font-medium text-green-800">{success}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="overflow-x-auto" id="tabla-documentos">
|
{/* Vista de tabla para pantallas grandes */}
|
||||||
<div style={{ minHeight: 'calc(7 * 56px)', maxHeight: 'calc(7 * 56px)', overflowY: currentDocuments.length > 8 ? 'auto' : 'hidden', position: 'relative' }}>
|
<div className="hidden lg:block">
|
||||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
|
<div className="overflow-x-auto">
|
||||||
<thead className="bg-gray-50 sticky top-0 z-20 shadow">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gradient-to-r from-gray-50 to-blue-50">
|
||||||
<tr>
|
<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-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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">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">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-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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Alerta</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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">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-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">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>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expediente</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
<td colSpan={8} className="px-6 py-12 text-center">
|
||||||
<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 }}>
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-gray-500 text-lg">Cargando documentos...</span>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
<td colSpan={8} className="px-6 py-12 text-center">
|
||||||
<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 }}>
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-danger-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : currentDocuments.length > 0 ? (
|
) : currentDocuments.length > 0 ? (
|
||||||
<>
|
currentDocuments.map(ped => (
|
||||||
{currentDocuments.map(ped => (
|
<tr key={ped.id} className="hover:bg-blue-50 transition-all duration-300 group">
|
||||||
<tr key={ped.id} className="hover:bg-blue-50 transition-all duration-300 hover:scale-[1.02] hover:shadow-md">
|
<td className="px-4 py-4 whitespace-nowrap">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<Link
|
||||||
<Link
|
to={`/expedientes/pedimento/${ped.id}`}
|
||||||
to={`/expedientes/pedimento/${ped.id}`}
|
className="text-blue-600 hover:text-blue-800 font-semibold transition-colors duration-200 group-hover:underline"
|
||||||
className="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200"
|
>
|
||||||
>
|
{ped.pedimento}
|
||||||
{ped.pedimento}
|
</Link>
|
||||||
</Link>
|
</td>
|
||||||
</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.fechapago}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{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-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.contribuyente}</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.curp_apoderado}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_total}</td>
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.saldo_disponible}</td>
|
||||||
ped.alerta
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_pedimento}</td>
|
||||||
? 'bg-red-100 text-red-800'
|
<td className="px-4 py-4 whitespace-nowrap">
|
||||||
: 'bg-green-100 text-green-800'
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
|
||||||
}`}>
|
ped.existe_expediente
|
||||||
{ped.alerta ? 'Sí' : 'No'}
|
? 'bg-green-100 text-green-800 border border-green-200'
|
||||||
</span>
|
: 'bg-gray-100 text-gray-600 border border-gray-200'
|
||||||
</td>
|
}`}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.curp_apoderado}</td>
|
{ped.existe_expediente ? (
|
||||||
<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>
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_pedimento}</td>
|
<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" />
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
</svg>
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
Sí
|
||||||
ped.existe_expediente
|
</>
|
||||||
? 'bg-green-100 text-green-800'
|
) : (
|
||||||
: 'bg-gray-100 text-gray-800'
|
<>
|
||||||
}`}>
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
{ped.existe_expediente ? 'Sí' : 'No'}
|
<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" />
|
||||||
</span>
|
</svg>
|
||||||
</td>
|
No
|
||||||
</tr>
|
</>
|
||||||
))}
|
)}
|
||||||
{/* Rellenar con filas vacías si hay menos de 8 */}
|
</span>
|
||||||
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
|
</td>
|
||||||
<tr key={`empty-${idx}`} className="">
|
</tr>
|
||||||
<td className="px-6 py-4 whitespace-nowrap" colSpan={9}> </td>
|
))
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
|
<td colSpan={8} className="px-6 py-12 text-center">
|
||||||
<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="flex flex-col items-center">
|
||||||
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No hay pedimentos</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay expedientes</h3>
|
||||||
<p className="text-gray-500">Aún no tienes pedimentos registrados.</p>
|
<p className="text-gray-500">No se encontraron expedientes con los filtros aplicados.</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -488,9 +511,97 @@ export default function Documents() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{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 totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
|
||||||
const maxPagesToShow = 5;
|
const maxPagesToShow = 5;
|
||||||
@@ -505,26 +616,26 @@ export default function Documents() {
|
|||||||
pageNumbers.push(i);
|
pageNumbers.push(i);
|
||||||
}
|
}
|
||||||
return (
|
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 flex-col sm:flex-row sm:items-center w-full gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
<label htmlFor="itemsPerPage" className="text-xs font-semibold text-gray-700">Registros por página:</label>
|
||||||
<select
|
<select
|
||||||
id="itemsPerPage"
|
id="itemsPerPage"
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
|
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 => (
|
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
|
||||||
<option key={size} value={size}>{size}</option>
|
<option key={size} value={size}>{size}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={e => handlePageChange(1, e)}
|
onClick={e => handlePageChange(1, e)}
|
||||||
disabled={currentPage === 1}
|
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>
|
</button>
|
||||||
@@ -532,26 +643,33 @@ export default function Documents() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={e => handlePageChange(currentPage - 1, e)}
|
onClick={e => handlePageChange(currentPage - 1, e)}
|
||||||
disabled={currentPage === 1}
|
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>
|
</button>
|
||||||
{pageNumbers.map(num => (
|
<div className="hidden sm:flex items-center gap-1">
|
||||||
<button
|
{pageNumbers.map(num => (
|
||||||
type="button"
|
<button
|
||||||
key={num}
|
type="button"
|
||||||
onClick={e => handlePageChange(num, e)}
|
key={num}
|
||||||
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'}`}
|
onClick={e => handlePageChange(num, e)}
|
||||||
disabled={num === currentPage}
|
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>
|
{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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={e => handlePageChange(currentPage + 1, e)}
|
onClick={e => handlePageChange(currentPage + 1, e)}
|
||||||
disabled={currentPage >= totalPages}
|
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>
|
</button>
|
||||||
@@ -559,11 +677,15 @@ export default function Documents() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={e => handlePageChange(totalPages, e)}
|
onClick={e => handlePageChange(totalPages, e)}
|
||||||
disabled={currentPage >= totalPages}
|
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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,202 +67,294 @@ export default function Organization() {
|
|||||||
}, [info]);
|
}, [info]);
|
||||||
|
|
||||||
if (loading) return (
|
if (loading) return (
|
||||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
<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">
|
<div className="text-center bg-white rounded-3xl shadow-2xl border border-gray-100 p-8 sm:p-12 max-w-md mx-auto">
|
||||||
<svg className="animate-spin h-12 w-12 text-navy-600 mx-auto mb-4" fill="none" viewBox="0 0 24 24">
|
<div className="relative inline-block mb-6">
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
<svg className="animate-spin h-16 w-16 text-blue-600 mx-auto" fill="none" viewBox="0 0 24 24">
|
||||||
<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>
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
</svg>
|
<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>
|
||||||
<p className="text-gray-600 text-lg">Cargando información de la organización...</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) return (
|
if (error) return (
|
||||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
<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-danger-50 border border-danger-200 rounded-xl p-6 max-w-md shadow-lg">
|
<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="flex items-center">
|
<div className="absolute inset-0 bg-gradient-to-br from-red-500/5 to-orange-500/5"></div>
|
||||||
<svg className="h-6 w-6 text-danger-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 text-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<div className="bg-red-100 rounded-full p-4 w-16 h-16 mx-auto mb-6 flex items-center justify-center">
|
||||||
</svg>
|
<svg className="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<p className="text-danger-800 font-medium">{error}</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 p-6">
|
<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-6xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header mejorado y decorativo */}
|
{/* 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="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-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
<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-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
<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">
|
||||||
Mi Organización
|
<span>Mi Organización</span>
|
||||||
{info && (
|
{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
|
{info.total_usuarios} usuarios
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h1>
|
</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>
|
</div>
|
||||||
{/* Efecto decorativo de fondo */}
|
{/* Efectos decorativos de fondo modernos */}
|
||||||
<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">
|
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
|
||||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
</div>
|
||||||
<defs>
|
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
|
||||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
|
||||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
</div>
|
||||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
{/* Partículas flotantes */}
|
||||||
</linearGradient>
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
</defs>
|
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
|
||||||
</svg>
|
<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 */}
|
{/* Animación personalizada para el icono y contador */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes bounce-slow {
|
@keyframes bounce-slow {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
50% { transform: translateY(-8px); }
|
50% { transform: translateY(-8px) scale(1.05); }
|
||||||
}
|
}
|
||||||
.animate-bounce-slow {
|
.animate-bounce-slow {
|
||||||
animation: bounce-slow 2.2s infinite;
|
animation: bounce-slow 3s infinite;
|
||||||
}
|
}
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from { opacity: 0; transform: scale(0.9); }
|
from { opacity: 0; transform: scale(0.9) translateY(10px); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
.animate-fade-in {
|
.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>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Barra de almacenamiento con color y progress bar */}
|
{/* Barra de almacenamiento con color y progress bar */}
|
||||||
<div className="mb-10">
|
<div className="mb-6 sm:mb-10">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
<div className="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 lg:p-8 relative overflow-hidden">
|
||||||
<svg className="w-6 h-6 text-success-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="absolute inset-0 bg-gradient-to-br from-green-500/3 to-blue-500/3"></div>
|
||||||
<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" />
|
<div className="relative z-10">
|
||||||
</svg>
|
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-4 sm:mb-6 flex items-center">
|
||||||
Uso de Almacenamiento
|
<div className="bg-gradient-to-br from-green-500 to-emerald-600 rounded-full p-2 sm:p-3 shadow-lg mr-3">
|
||||||
</h2>
|
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div className="relative w-full h-8 bg-gray-200 rounded-full overflow-hidden shadow-inner">
|
<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" />
|
||||||
{/* Progress bar de color dinámico según porcentaje */}
|
</svg>
|
||||||
{(() => {
|
</div>
|
||||||
const used = info?.espacio_utilizado_gb || 0;
|
Uso de Almacenamiento
|
||||||
const limit = info?.limite_almacenamiento_gb || 1;
|
</h2>
|
||||||
const percent = Math.min(100, (100 * used / limit));
|
|
||||||
let barColor = 'linear-gradient(90deg, #22c55e 0%, #16a34a 100%)'; // verde
|
{/* Estadísticas rápidas */}
|
||||||
if (animatedPercent >= 80) {
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||||
barColor = 'linear-gradient(90deg, #ef4444 0%, #b91c1c 100%)'; // rojo
|
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-2xl p-4 border border-green-100">
|
||||||
} else if (animatedPercent >= 50) {
|
<div className="flex items-center gap-3">
|
||||||
barColor = 'linear-gradient(90deg, #f59e42 0%, #d97706 100%)'; // naranja
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
}
|
<span className="text-sm font-medium text-gray-600">Usado</span>
|
||||||
return (
|
</div>
|
||||||
<div
|
<div className="text-2xl font-bold text-green-700 mt-1">
|
||||||
className="absolute left-0 top-0 h-8 rounded-full shadow-lg transition-all duration-700"
|
{info?.espacio_utilizado_gb?.toFixed(2)} GB
|
||||||
style={{ width: `${animatedPercent}%`, background: barColor }}
|
</div>
|
||||||
></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">
|
||||||
{/* Etiquetas sobre la barra */}
|
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
|
||||||
<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-sm font-medium text-gray-600">Disponible</span>
|
||||||
<span className="text-success-700 flex items-center">
|
</div>
|
||||||
<span className="inline-block w-3 h-3 rounded-full bg-gradient-to-br from-green-400 to-green-600 mr-2"></span>
|
<div className="text-2xl font-bold text-blue-700 mt-1">
|
||||||
{info?.espacio_utilizado_gb?.toFixed(2)} GB usados
|
{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB
|
||||||
</span>
|
</div>
|
||||||
<span className="text-gray-700">{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB libres</span>
|
</div>
|
||||||
<span className="text-gray-500">{info?.limite_almacenamiento_gb} GB límite</span>
|
<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>
|
||||||
</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 */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<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" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Tarjeta Usuarios */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<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" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Tarjeta Pedimentos */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-6a2 2 0 012-2h2a2 2 0 012 2v6m-4 0h4" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Tarjeta Documentos */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<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" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
{/* Tarjeta Límite de Almacenamiento */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Tarjeta Espacio Utilizado */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v8m0 0a4 4 0 100-8 4 4 0 000 8z" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Tarjeta Espacio Disponible */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 20V4m0 0a8 8 0 110 16 8 8 0 010-16z" />
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Tarjeta Porcentaje Utilizado */}
|
{/* 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="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="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">
|
<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>
|
||||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative z-10 w-full flex flex-col items-center">
|
||||||
<circle cx="12" cy="12" r="10" strokeWidth="2" />
|
<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">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6l4 2" />
|
<svg className="w-6 h-6 sm:w-7 sm:h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -64,9 +64,14 @@ export default function Procesos() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access');
|
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 = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
'Authorization': `Bearer ${token}`,
|
||||||
};
|
};
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento,
|
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
|
// Cierra el dropdown si se hace click fuera
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openDropdownId === null) return;
|
if (openDropdownId === null) return;
|
||||||
function handleClick(e) {
|
function handleClickOutside(e) {
|
||||||
const el = document.getElementById(`dropdown-acciones-${openDropdownId}`);
|
const el = document.getElementById(`dropdown-acciones-${openDropdownId}`);
|
||||||
if (el && !el.contains(e.target)) {
|
if (el && !el.contains(e.target)) {
|
||||||
setOpenDropdownId(null);
|
setOpenDropdownId(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('mousedown', handleClick);
|
// Usar setTimeout para evitar que el click que abre el dropdown lo cierre inmediatamente
|
||||||
return () => document.removeEventListener('mousedown', handleClick);
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
}, 100);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
};
|
||||||
}, [openDropdownId]);
|
}, [openDropdownId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -106,6 +116,10 @@ export default function Procesos() {
|
|||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access');
|
const token = localStorage.getItem('access');
|
||||||
|
if (!token) {
|
||||||
|
setError('No hay token de autenticación. Por favor, inicia sesión nuevamente.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Construir query params
|
// Construir query params
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('page', String(page));
|
params.append('page', String(page));
|
||||||
@@ -117,10 +131,21 @@ export default function Procesos() {
|
|||||||
params.append('ordering', (sortOrder === 'desc' ? '-' : '') + sortField);
|
params.append('ordering', (sortOrder === 'desc' ? '-' : '') + sortField);
|
||||||
}
|
}
|
||||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
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 });
|
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();
|
const data = await res.json();
|
||||||
|
console.log('Data received:', data);
|
||||||
setProcesos(data.results || []);
|
setProcesos(data.results || []);
|
||||||
setCount(data.count || 0);
|
setCount(data.count || 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -133,41 +158,48 @@ export default function Procesos() {
|
|||||||
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]);
|
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]);
|
||||||
|
|
||||||
return (
|
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="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' }}>
|
{/* Header mejorado y responsivo */}
|
||||||
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
<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"
|
||||||
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h1 className="text-3xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
<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">
|
||||||
Procesos del Sistema
|
<span>Procesos del Sistema</span>
|
||||||
<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 > 0 && (
|
||||||
{count}
|
<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">
|
||||||
</span>
|
{count} procesos
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h1>
|
</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>
|
||||||
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
|
{/* Efectos decorativos de fondo modernos */}
|
||||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
|
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
|
||||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
|
||||||
<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>
|
||||||
|
<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>{`
|
<style>{`
|
||||||
@keyframes bounce-slow {
|
@keyframes bounce-slow {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
50% { transform: translateY(-8px); }
|
50% { transform: translateY(-8px) scale(1.05); }
|
||||||
}
|
}
|
||||||
.animate-bounce-slow {
|
.animate-bounce-slow {
|
||||||
animation: bounce-slow 2.2s infinite;
|
animation: bounce-slow 3s infinite;
|
||||||
}
|
}
|
||||||
@keyframes fadein-slideup {
|
@keyframes fadein-slideup {
|
||||||
0% { opacity: 0; transform: translateY(40px); }
|
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;
|
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
|
||||||
}
|
}
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from { opacity: 0; transform: scale(0.9); }
|
from { opacity: 0; transform: scale(0.9) translateY(10px); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fade-in 0.7s ease;
|
animation: fade-in 0.8s ease-out;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</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' }}>
|
{/* Contenido principal */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<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"
|
||||||
<h2 className="text-2xl font-bold text-blue-800">Procesamiento de Pedimentos</h2>
|
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>
|
</div>
|
||||||
{/* Filtros */}
|
|
||||||
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
|
{/* Filtros responsivos mejorados */}
|
||||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
<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">
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
|
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||||||
<input
|
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
type="text"
|
<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" />
|
||||||
value={pedimentoPedimentoFilter}
|
</svg>
|
||||||
onChange={e => setPedimentoPedimentoFilter(e.target.value)}
|
Filtros de búsqueda
|
||||||
placeholder="Buscar por pedimento..."
|
</h3>
|
||||||
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 className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
/>
|
<div className="space-y-2">
|
||||||
</div>
|
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Estado</label>
|
Pedimento
|
||||||
<select
|
</label>
|
||||||
value={estadoFilter}
|
<input
|
||||||
onChange={e => setEstadoFilter(e.target.value)}
|
type="text"
|
||||||
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"
|
value={pedimentoPedimentoFilter}
|
||||||
>
|
onChange={e => setPedimentoPedimentoFilter(e.target.value)}
|
||||||
<option value="">Todos</option>
|
placeholder="Buscar por pedimento..."
|
||||||
<option value="1">En Espera</option>
|
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="2">Procesando</option>
|
/>
|
||||||
<option value="3">Finalizado</option>
|
</div>
|
||||||
<option value="4">Error</option>
|
<div className="space-y-2">
|
||||||
</select>
|
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||||
</div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
Estado
|
||||||
<label className="text-xs font-semibold text-gray-700 mb-1">Servicio</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={servicioFilter}
|
value={estadoFilter}
|
||||||
onChange={e => setServicioFilter(e.target.value)}
|
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"
|
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</option>
|
<option value="">Todos los estados</option>
|
||||||
<option value="1">Estado de pedimento</option>
|
<option value="1">En Espera</option>
|
||||||
<option value="2">Listado de pedimentos</option>
|
<option value="2">Procesando</option>
|
||||||
<option value="3">Pedimento Completo</option>
|
<option value="3">Finalizado</option>
|
||||||
<option value="4">Pedimento Partidas</option>
|
<option value="4">Error</option>
|
||||||
<option value="5">Pedimento Remesas</option>
|
</select>
|
||||||
<option value="6">Acuse</option>
|
</div>
|
||||||
<option value="7">EDocument</option>
|
<div className="space-y-2 sm:col-span-2 lg:col-span-1">
|
||||||
<option value="8">Cove</option>
|
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||||
<option value="9">Acuse Cove</option>
|
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
|
||||||
|
Servicio
|
||||||
</select>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Estados de carga y error mejorados */}
|
||||||
{loading ? (
|
{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 ? (
|
) : 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' }}>
|
{/* Vista de tabla para pantallas grandes */}
|
||||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
|
<div className="hidden lg:block overflow-x-auto bg-white rounded-2xl border border-gray-200 shadow-sm">
|
||||||
<thead className="bg-gradient-to-r from-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-slate-50">
|
||||||
<tr>
|
<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={() => {
|
onClick={() => {
|
||||||
setSortField('id');
|
setSortField('id');
|
||||||
setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc');
|
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>
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
setSortField('organizacion_name');
|
setSortField('organizacion_name');
|
||||||
setSortOrder(sortField === 'organizacion_name' && sortOrder === 'asc' ? 'desc' : 'asc');
|
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>
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
setSortField('estado');
|
setSortField('estado');
|
||||||
setSortOrder(sortField === 'estado' && sortOrder === 'asc' ? 'desc' : 'asc');
|
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>
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
setSortField('pedimento');
|
setSortField('pedimento');
|
||||||
setSortOrder(sortField === 'pedimento' && sortOrder === 'asc' ? 'desc' : 'asc');
|
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>
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
setSortField('servicio');
|
setSortField('servicio');
|
||||||
setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc');
|
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>
|
||||||
<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
|
Acciones
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(12 * 40px)' }}>
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
{procesos.length === 0 ? (
|
{procesos.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="text-center py-8 text-gray-500">No hay registros</td>
|
<td colSpan={6} className="text-center py-12">
|
||||||
</tr>
|
<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">
|
||||||
procesos.map((proc) => (
|
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<tr key={proc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
|
<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" />
|
||||||
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">{proc.id}</td>
|
</svg>
|
||||||
<td className="px-2 py-2 whitespace-nowrap align-middle">{proc.organizacion_name || '-'}</td>
|
</div>
|
||||||
<td className="px-2 py-2 whitespace-nowrap align-middle">{
|
<p className="text-gray-500 font-medium">No hay procesos disponibles</p>
|
||||||
proc.estado === 1 ? 'En Espera'
|
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
|
||||||
: 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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
) : (
|
||||||
)}
|
procesos.map((proc) => (
|
||||||
</tbody>
|
<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>
|
</table>
|
||||||
</div>
|
</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 && (
|
{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 totalPages = Math.max(1, Math.ceil(count / itemsPerPage));
|
||||||
const maxPagesToShow = 5;
|
const maxPagesToShow = 5;
|
||||||
@@ -385,26 +657,26 @@ export default function Procesos() {
|
|||||||
pageNumbers.push(i);
|
pageNumbers.push(i);
|
||||||
}
|
}
|
||||||
return (
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<label htmlFor="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
<label htmlFor="itemsPerPage" className="text-sm text-gray-600 font-medium">Registros por página:</label>
|
||||||
<select
|
<select
|
||||||
id="itemsPerPage"
|
id="itemsPerPage"
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={e => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
|
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 => (
|
{[5, 8, 12, 20, 50, 100].map(size => (
|
||||||
<option key={size} value={size}>{size}</option>
|
<option key={size} value={size}>{size}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={e => { e.preventDefault(); setPage(1); }}
|
onClick={e => { e.preventDefault(); setPage(1); }}
|
||||||
disabled={page === 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>
|
</button>
|
||||||
@@ -412,7 +684,7 @@ export default function Procesos() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }}
|
onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }}
|
||||||
disabled={page === 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>
|
</button>
|
||||||
@@ -421,7 +693,7 @@ export default function Procesos() {
|
|||||||
type="button"
|
type="button"
|
||||||
key={num}
|
key={num}
|
||||||
onClick={e => { e.preventDefault(); setPage(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}
|
disabled={num === page}
|
||||||
>
|
>
|
||||||
{num}
|
{num}
|
||||||
@@ -431,7 +703,7 @@ export default function Procesos() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={e => { e.preventDefault(); setPage(p => p + 1); }}
|
onClick={e => { e.preventDefault(); setPage(p => p + 1); }}
|
||||||
disabled={page >= 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>
|
</button>
|
||||||
@@ -439,18 +711,20 @@ export default function Procesos() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={e => { e.preventDefault(); setPage(totalPages); }}
|
onClick={e => { e.preventDefault(); setPage(totalPages); }}
|
||||||
disabled={page >= 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>
|
</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>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -332,37 +332,40 @@ export default function Users() {
|
|||||||
const activeCount = users.filter(u => u.is_active === true).length;
|
const activeCount = users.filter(u => u.is_active === true).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="min-h-screen p-4 sm:p-6 bg-gradient-to-br from-gray-50 to-blue-50">
|
||||||
{/* Header Mejorado */}
|
<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">
|
{/* Header Mejorado */}
|
||||||
<div className="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
<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">
|
||||||
<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">
|
||||||
<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 className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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>
|
||||||
<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 */}
|
{/* Animación personalizada para el icono */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes bounce-slow {
|
@keyframes bounce-slow {
|
||||||
@@ -381,73 +384,81 @@ export default function Users() {
|
|||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
{/* Stats Cards con animación */}
|
{/* Stats Cards con animación */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<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 rounded-lg fade-in-up-users transition-all duration-500 hover:scale-105 hover:shadow-2xl cursor-pointer">
|
<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-5">
|
<div className="p-4 sm:p-5">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
<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 className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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" />
|
||||||
</div>
|
</svg>
|
||||||
<div className="ml-5 w-0 flex-1">
|
</div>
|
||||||
<dl>
|
</div>
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Usuarios</dt>
|
<div className="ml-4 w-0 flex-1">
|
||||||
<dd className="text-lg font-medium text-gray-900">{users.length}</dd>
|
<dl>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
<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="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-4 sm:p-5">
|
||||||
<div className="p-5">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex-shrink-0">
|
||||||
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-5 w-0 flex-1">
|
</div>
|
||||||
<dl>
|
<div className="ml-4 w-0 flex-1">
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Activos</dt>
|
<dl>
|
||||||
<dd className="text-lg font-medium text-gray-900">{activeCount}</dd>
|
<dt className="text-xs sm:text-sm font-medium text-gray-500 truncate">Activos</dt>
|
||||||
</dl>
|
<dd className="text-lg sm:text-xl font-bold text-gray-900">{activeCount}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<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="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-4 sm:p-5">
|
||||||
<div className="p-5">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex-shrink-0">
|
||||||
<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-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-5 w-0 flex-1">
|
</div>
|
||||||
<dl>
|
<div className="ml-4 w-0 flex-1">
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Con Perfil Completo</dt>
|
<dl>
|
||||||
<dd className="text-lg font-medium text-gray-900">
|
<dt className="text-xs sm:text-sm font-medium text-gray-500 truncate">Completos</dt>
|
||||||
{users.filter(u => u.first_name && u.last_name).length}
|
<dd className="text-lg sm:text-xl font-bold text-gray-900">
|
||||||
</dd>
|
{users.filter(u => u.first_name && u.last_name).length}
|
||||||
</dl>
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<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="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-4 sm:p-5">
|
||||||
<div className="p-5">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex-shrink-0">
|
||||||
<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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-5 w-0 flex-1">
|
</div>
|
||||||
<dl>
|
<div className="ml-4 w-0 flex-1">
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Última Semana</dt>
|
<dl>
|
||||||
<dd className="text-lg font-medium text-gray-900">
|
<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}
|
{users.filter(u => u.id % 3 === 0).length}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -457,90 +468,93 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and Actions */}
|
{/* Search and Actions */}
|
||||||
<div className="bg-white shadow rounded-lg mb-6">
|
<div className="bg-white shadow-lg rounded-xl mb-6 border border-gray-100">
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
<div className="px-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200">
|
||||||
{/* Barra de búsqueda principal */}
|
{/* Barra de búsqueda principal */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4">
|
<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-md">
|
<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="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">
|
<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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</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>
|
||||||
)}
|
<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>
|
||||||
|
<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>
|
||||||
<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 */}
|
{/* Filtros avanzados */}
|
||||||
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
<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 flex-col space-y-3">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-sm font-medium text-gray-700">Filtrar por:</span>
|
<span className="text-sm font-medium text-gray-700">Filtrar por:</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
</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: '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: '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: '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: '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 }
|
{ key: 'inactive', label: 'Inactivos', count: users.filter(u => u.is_active === false).length }
|
||||||
].map(filter => (
|
].map(filter => (
|
||||||
<button
|
<button
|
||||||
key={filter.key}
|
key={filter.key}
|
||||||
onClick={() => setStatusFilter(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
|
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'
|
? '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'
|
: '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="truncate">{filter.label}</span>
|
||||||
<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={`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}
|
{filter.count}
|
||||||
</span>
|
</span>
|
||||||
@@ -549,7 +563,6 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Información de resultados */}
|
{/* Información de resultados */}
|
||||||
{(debouncedSearchTerm || statusFilter !== 'all') && (
|
{(debouncedSearchTerm || statusFilter !== 'all') && (
|
||||||
@@ -585,17 +598,19 @@ export default function Users() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table con animación y layout SPA fijo */}
|
{/* Vista responsiva: tabla para desktop, cards para mobile */}
|
||||||
<div className="overflow-hidden">
|
|
||||||
|
{/* 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' }}>
|
<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">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50 sticky top-0 z-20">
|
<thead className="bg-gradient-to-r from-gray-50 to-blue-50 sticky top-0 z-20">
|
||||||
<tr>
|
<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-bold text-blue-700 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-bold text-blue-700 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-bold text-blue-700 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-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-medium text-gray-500 uppercase tracking-wider">Acciones</th>
|
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
<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>
|
<tr>
|
||||||
<td colSpan={5} 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 }}>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -622,7 +637,7 @@ export default function Users() {
|
|||||||
<tr
|
<tr
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className={
|
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')
|
(idx % 2 === 0 ? ' bg-white' : ' bg-gray-50')
|
||||||
}
|
}
|
||||||
style={{ animationDelay: `${0.05 * idx}s` }}
|
style={{ animationDelay: `${0.05 * idx}s` }}
|
||||||
@@ -680,7 +695,7 @@ export default function Users() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(user.is_staff || user.is_superuser) && (
|
{(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">
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -700,11 +715,10 @@ export default function Users() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
{/* Botón Editar eliminado */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowDeleteModal(true); setUserToDelete(user); }}
|
onClick={() => { setShowDeleteModal(true); setUserToDelete(user); }}
|
||||||
disabled={user.username === localStorage.getItem('username')}
|
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'}
|
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">
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
{/* Rellenar con filas vacías si hay menos de 8 */}
|
{/* Rellenar con filas vacías si hay menos de 8 */}
|
||||||
{currentUsers.length < 8 && !loading && !error && Array.from({ length: 8 - currentUsers.length }).map((_, idx) => (
|
{currentUsers.length < 8 && !loading && !error && Array.from({ length: 8 - currentUsers.length }).map((_, idx) => (
|
||||||
@@ -744,79 +757,169 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Paginación */}
|
{/* Cards para pantallas pequeñas */}
|
||||||
{totalUsers > 0 && (
|
<div className="lg:hidden">
|
||||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
{loading ? (
|
||||||
<div className="flex items-center mb-4 sm:mb-0">
|
<div className="flex items-center justify-center py-12">
|
||||||
<span className="text-sm text-gray-700 mr-4">
|
<div className="text-center">
|
||||||
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
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
</span>
|
<span className="text-gray-500 text-lg">Cargando usuarios...</span>
|
||||||
<select
|
</div>
|
||||||
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>
|
|
||||||
</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">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||||
disabled={currentPage === 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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
|
<div className="flex space-x-1">
|
||||||
<div className="hidden sm:flex space-x-1">
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
|
||||||
{[...Array(totalPages)].map((_, index) => {
|
<button
|
||||||
const page = index + 1;
|
key={page}
|
||||||
const isCurrentPage = page === currentPage;
|
onClick={() => setCurrentPage(page)}
|
||||||
const isNearCurrentPage = Math.abs(page - currentPage) <= 2;
|
className={`px-3 py-2 text-sm font-medium rounded-lg transition-all duration-200 ${
|
||||||
const isFirstOrLast = page === 1 || page === totalPages;
|
currentPage === page
|
||||||
|
? 'bg-blue-600 text-white shadow-lg transform scale-105'
|
||||||
if (totalPages <= 7 || isNearCurrentPage || isFirstOrLast) {
|
: 'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 hover:transform hover:scale-105'
|
||||||
return (
|
}`}
|
||||||
<button
|
>
|
||||||
key={page}
|
{page}
|
||||||
onClick={() => handlePageChange(page)}
|
</button>
|
||||||
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>
|
</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
|
<button
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
||||||
disabled={currentPage === 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
|
Siguiente
|
||||||
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -890,7 +993,7 @@ export default function Users() {
|
|||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
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">
|
<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" />
|
<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}
|
value={form.username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="nombre_usuario"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -950,7 +1053,7 @@ export default function Users() {
|
|||||||
value={form.email}
|
value={form.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="usuario@ejemplo.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -986,7 +1089,7 @@ export default function Users() {
|
|||||||
name="first_name"
|
name="first_name"
|
||||||
value={form.first_name}
|
value={form.first_name}
|
||||||
onChange={handleChange}
|
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"
|
placeholder="Nombre"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1000,7 +1103,7 @@ export default function Users() {
|
|||||||
name="last_name"
|
name="last_name"
|
||||||
value={form.last_name}
|
value={form.last_name}
|
||||||
onChange={handleChange}
|
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"
|
placeholder="Apellido"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1015,7 +1118,7 @@ export default function Users() {
|
|||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="Contraseña del usuario"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1026,14 +1129,14 @@ export default function Users() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={submitting}
|
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
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
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 && (
|
{submitting && (
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
<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}
|
value={form.username}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="nombre_usuario"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1091,7 +1194,7 @@ export default function Users() {
|
|||||||
value={form.email}
|
value={form.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
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"
|
placeholder="usuario@ejemplo.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1105,7 +1208,7 @@ export default function Users() {
|
|||||||
name="first_name"
|
name="first_name"
|
||||||
value={form.first_name}
|
value={form.first_name}
|
||||||
onChange={handleChange}
|
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"
|
placeholder="Nombre"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1119,7 +1222,7 @@ export default function Users() {
|
|||||||
name="last_name"
|
name="last_name"
|
||||||
value={form.last_name}
|
value={form.last_name}
|
||||||
onChange={handleChange}
|
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"
|
placeholder="Apellido"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1133,7 +1236,7 @@ export default function Users() {
|
|||||||
name="password"
|
name="password"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={handleChange}
|
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"
|
placeholder="Dejar vacío para mantener actual"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
@@ -1147,14 +1250,14 @@ export default function Users() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={submitting}
|
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
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
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 && (
|
{submitting && (
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
<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
|
<button
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={submitting}
|
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
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
@@ -1238,6 +1341,7 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function Vucem() {
|
|||||||
usuario: '',
|
usuario: '',
|
||||||
password: '',
|
password: '',
|
||||||
patente: '',
|
patente: '',
|
||||||
|
efirma: '',
|
||||||
key: null,
|
key: null,
|
||||||
cer: null,
|
cer: null,
|
||||||
is_importador: false,
|
is_importador: false,
|
||||||
@@ -84,36 +85,63 @@ export default function Vucem() {
|
|||||||
// Reset page si cambia el filtro
|
// Reset page si cambia el filtro
|
||||||
useEffect(() => { setPage(1); }, [filterUsuario, filterPatente]);
|
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
|
// Table y header estilo Users.jsx
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-6 max-w-7xl mx-auto">
|
||||||
{/* Header Mejorado igual que Users.jsx */}
|
{/* Header modernizado con gradientes azules */}
|
||||||
<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="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="flex-shrink-0 bg-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-blue-900/30"></div>
|
||||||
<svg className="h-10 w-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="absolute -top-24 -right-24 w-48 h-48 bg-white/10 rounded-full blur-xl"></div>
|
||||||
<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" />
|
<div className="absolute -bottom-12 -left-12 w-32 h-32 bg-white/5 rounded-full blur-lg"></div>
|
||||||
</svg>
|
|
||||||
</div>
|
<div className="relative flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6">
|
||||||
<div>
|
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-2xl p-3 sm:p-4 shadow-lg animate-bounce-slow">
|
||||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
VUCEM
|
<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" />
|
||||||
<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>
|
</svg>
|
||||||
<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">
|
</div>
|
||||||
Activos: {vucemList.filter(v => v.is_active).length}
|
<div className="flex-1 min-w-0">
|
||||||
</span>
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
|
||||||
</h1>
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-white tracking-tight">
|
||||||
<p className="text-lg text-blue-700/80 font-medium">Gestiona y supervisa los accesos y certificados VUCEM registrados en el sistema.</p>
|
Credenciales VU
|
||||||
</div>
|
</h1>
|
||||||
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
|
<div className="flex flex-wrap gap-2">
|
||||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
|
<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">
|
||||||
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<defs>
|
<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" />
|
||||||
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
</svg>
|
||||||
<stop stopColor="#3b82f6" stopOpacity="0.15" />
|
{vucemList.length} Total
|
||||||
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
|
</span>
|
||||||
</linearGradient>
|
<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' }}>
|
||||||
</defs>
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
</svg>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<style>{`
|
<style>{`
|
||||||
@@ -131,190 +159,453 @@ export default function Vucem() {
|
|||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fade-in 0.7s ease;
|
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>
|
`}</style>
|
||||||
{/* Filtros y botón Crear VUCEM igual que Users.jsx */}
|
{/* Controles de búsqueda y filtros mejorados */}
|
||||||
<div className="bg-white shadow rounded-lg mb-6">
|
<div className="bg-white shadow-lg rounded-xl border border-gray-100 mb-6 overflow-hidden">
|
||||||
<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="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-1 min-w-0 max-w-md">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||||
<div className="relative bg-gray-50 rounded-lg border border-gray-200 shadow-sm">
|
{/* Búsqueda principal */}
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="flex-1 max-w-md">
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="relative">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z" />
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
</svg>
|
<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>
|
</div>
|
||||||
<input
|
</div>
|
||||||
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"
|
{/* Información y botón crear */}
|
||||||
placeholder="Buscar por usuario o patente..."
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||||
value={filterUsuario}
|
{filteredList.length !== vucemList.length && (
|
||||||
onChange={e => setFilterUsuario(e.target.value)}
|
<div className="text-sm text-blue-700">
|
||||||
autoComplete="off"
|
<span className="inline-flex items-center">
|
||||||
/>
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
{filterUsuario && (
|
<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" />
|
||||||
<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" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
Mostrando {filteredList.length} de {vucemList.length} registros
|
||||||
|
{filterUsuario && <span className="ml-1">para "{filterUsuario}"</span>}
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
</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>
|
</div>
|
||||||
{/* Tabla igual que Users.jsx */}
|
{/* Vista responsiva: tabla para desktop, cards para mobile */}
|
||||||
<div className="overflow-hidden">
|
|
||||||
|
{/* 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' }}>
|
<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">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50 sticky top-0 z-20">
|
<thead className="bg-gradient-to-r from-gray-50 to-blue-50 sticky top-0 z-20">
|
||||||
<tr>
|
<tr>
|
||||||
<th 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-bold text-blue-700 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 scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 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 scope="col" className="px-6 py-3 text-left text-xs font-bold text-blue-700 uppercase tracking-wider">Archivos</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cer</th>
|
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Estado</th>
|
||||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Activo</th>
|
<th scope="col" className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider">Acciones</th>
|
||||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<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 }}>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<tr>
|
<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 }}>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : paginatedList.length > 0 ? (
|
) : paginatedList.length > 0 ? (
|
||||||
paginatedList.map((vucem, idx) => (
|
<>
|
||||||
<tr
|
{paginatedList.map((vucem, idx) => (
|
||||||
key={vucem.id}
|
<tr
|
||||||
className={
|
key={vucem.id}
|
||||||
`transition-all duration-300 hover:scale-[1.025] hover:shadow-lg hover:bg-indigo-50 fade-in-up-users` +
|
className={
|
||||||
(idx % 2 === 0 ? ' bg-white' : ' bg-gray-50')
|
`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` }}
|
}
|
||||||
>
|
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">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<div className="flex items-center">
|
||||||
{vucem.key ? (
|
<div className="flex-shrink-0 h-10 w-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
<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="h-5 w-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
<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" />
|
||||||
<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>
|
||||||
</svg>
|
</div>
|
||||||
Archivo cargado
|
<div className="ml-4">
|
||||||
</span>
|
<div className="text-sm font-medium text-gray-900">{vucem.usuario}</div>
|
||||||
) : (
|
<div className="text-sm text-gray-500">ID: {vucem.id}</div>
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-400">
|
</div>
|
||||||
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
</div>
|
||||||
<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" />
|
</td>
|
||||||
</svg>
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
Sin archivo
|
<div className="text-sm font-medium text-gray-900">{vucem.patente}</div>
|
||||||
</span>
|
<div className="text-sm text-gray-500">e.firma: {vucem.efirma}</div>
|
||||||
)}
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
<div className="flex flex-col gap-1">
|
||||||
{vucem.cer ? (
|
{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">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Archivo cargado
|
Key
|
||||||
</span>
|
</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">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
Sin archivo
|
Sin Key
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
{vucem.cer ? (
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
{vucem.is_active ? (
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
<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 className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
</svg>
|
||||||
<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" />
|
Cer
|
||||||
</svg>
|
</span>
|
||||||
Activo
|
) : (
|
||||||
</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">
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
<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 className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
</svg>
|
||||||
<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" />
|
Sin Cer
|
||||||
</svg>
|
</span>
|
||||||
Inactivo
|
)}
|
||||||
</span>
|
</div>
|
||||||
)}
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<button onClick={() => { setEditVucem(vucem); setShowEditModal(true); }} className="text-indigo-600 hover:underline font-semibold mr-3">Editar</button>
|
{vucem.is_active ? (
|
||||||
<button onClick={() => { setDeleteVucem(vucem); setShowDeleteModal(true); }} className="text-red-600 hover:underline font-semibold">Eliminar</button>
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
</td>
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
</tr>
|
<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}> </td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Paginación igual que Users.jsx */}
|
{/* Cards para pantallas pequeñas */}
|
||||||
{filteredList.length > 0 && (
|
<div className="lg:hidden">
|
||||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
{loading ? (
|
||||||
<div className="flex items-center mb-4 sm:mb-0">
|
<div className="flex items-center justify-center py-12">
|
||||||
<span className="text-sm text-gray-700">
|
<div className="text-center">
|
||||||
Mostrando <span className="font-semibold">{filteredList.length}</span> de <span className="font-semibold">{vucemList.length}</span> registros
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
</span>
|
<span className="text-gray-500 text-lg">Cargando registros VUCEM...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
) : error ? (
|
||||||
<button
|
<div className="flex items-center justify-center py-12">
|
||||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
<div className="text-center">
|
||||||
disabled={page === 1}
|
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
||||||
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'}`}
|
<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" />
|
||||||
Anterior
|
</svg>
|
||||||
</button>
|
</div>
|
||||||
<span className="mx-2 text-base font-medium text-gray-700">Página {page} de {totalPages}</span>
|
<span className="text-red-600 text-lg">Error: {error}</span>
|
||||||
<button
|
</div>
|
||||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
</div>
|
||||||
disabled={page === totalPages}
|
) : paginatedList.length > 0 ? (
|
||||||
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'}`}
|
<div className="space-y-4">
|
||||||
>
|
{paginatedList.map((vucem, idx) => (
|
||||||
Siguiente
|
<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` }}>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal de creación */}
|
{/* Modal de creación */}
|
||||||
{showCreateModal && (
|
{showCreateModal && (
|
||||||
@@ -332,6 +623,7 @@ export default function Vucem() {
|
|||||||
formData.append('usuario', form.usuario);
|
formData.append('usuario', form.usuario);
|
||||||
formData.append('password', form.password);
|
formData.append('password', form.password);
|
||||||
formData.append('patente', form.patente);
|
formData.append('patente', form.patente);
|
||||||
|
formData.append('efirma', form.efirma);
|
||||||
if (form.key) formData.append('key', form.key);
|
if (form.key) formData.append('key', form.key);
|
||||||
if (form.cer) formData.append('cer', form.cer);
|
if (form.cer) formData.append('cer', form.cer);
|
||||||
formData.append('is_importador', form.is_importador);
|
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>
|
<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" />
|
<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>
|
||||||
|
<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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Key (.key)</label>
|
<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" />
|
<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>
|
</div>
|
||||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
<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="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-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="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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -418,6 +714,7 @@ export default function Vucem() {
|
|||||||
formData.append('usuario', form.usuario);
|
formData.append('usuario', form.usuario);
|
||||||
if (form.password) formData.append('password', form.password);
|
if (form.password) formData.append('password', form.password);
|
||||||
formData.append('patente', form.patente);
|
formData.append('patente', form.patente);
|
||||||
|
formData.append('efirma', form.efirma);
|
||||||
if (form.key) formData.append('key', form.key);
|
if (form.key) formData.append('key', form.key);
|
||||||
if (form.cer) formData.append('cer', form.cer);
|
if (form.cer) formData.append('cer', form.cer);
|
||||||
formData.append('is_importador', form.is_importador);
|
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>
|
<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" />
|
<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>
|
||||||
|
<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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Key (.key)</label>
|
<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" />
|
<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>
|
</div>
|
||||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
<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="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-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="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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -505,7 +806,7 @@ export default function Vucem() {
|
|||||||
<p className="text-sm text-red-600">Esta acción no se puede deshacer.</p>
|
<p className="text-sm text-red-600">Esta acción no se puede deshacer.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center space-x-3 pt-4">
|
<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 () => {
|
<button type="button" onClick={async () => {
|
||||||
if (!deleteVucem) return;
|
if (!deleteVucem) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user