diff --git a/.env b/.env index 0b5fb77..bae8a6f 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ VITE_DEBUG_MODE=false -VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1 -VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservice/api/v1 +VITE_EFC_API_URL=http://192.168.1.195:8000/api/v1 +VITE_EFC_MICROSERVICE_URL=http://192.168.1.195:8001/api/v1 diff --git a/src/components/ResponsiveSidebar.jsx b/src/components/ResponsiveSidebar.jsx new file mode 100644 index 0000000..b241374 --- /dev/null +++ b/src/components/ResponsiveSidebar.jsx @@ -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 ( +
+ {/* Sidebar */} + + + {/* Contenido principal */} +
+ {/* Header móvil - siempre visible en móviles */} +
+ +

EFC Dashboard

+
{/* Spacer para centrar el título */} +
+ + {/* Contenido */} +
+ {children} +
+
+
+ ); +} diff --git a/src/components/SIDEBAR_README.md b/src/components/SIDEBAR_README.md new file mode 100644 index 0000000..d1320be --- /dev/null +++ b/src/components/SIDEBAR_README.md @@ -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 ( + +

Contenido de tu aplicación

+

Todo tu contenido va aquí...

+
+ ); +} +``` + +**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 ( +
+ {/* ¡Ahora funciona automáticamente en móviles! */} +
+ Tu contenido aquí... +
+
+ ); +} +``` + +**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 ``: +✅ **No necesitas cambiar nada!** El sidebar ahora funciona automáticamente en móviles. + +### Si quieres el header móvil: +```jsx +// Cambia esto: +
+ +
Contenido
+
+ +// Por esto: + + Contenido + +``` + +## 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! 📱💻 diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 000d8ab..3a9436d 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useUser } from '../context/UserContext'; -export default function Sidebar() { +export default function Sidebar({ isMobileOpen, onMobileClose }) { // Leer si el usuario es importador desde localStorage const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'; // Leer grupos del usuario desde localStorage @@ -18,7 +18,15 @@ export default function Sidebar() { const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5); // Leer DEBUG_MODE desde variables de entorno const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true'; + + // Estados para responsividad const [isCollapsed, setIsCollapsed] = useState(false); + const [internalMobileOpen, setInternalMobileOpen] = useState(false); + + // Usar estado interno si no se pasan props + const mobileOpen = isMobileOpen !== undefined ? isMobileOpen : internalMobileOpen; + const handleMobileClose = onMobileClose || (() => setInternalMobileOpen(false)); + const handleMobileOpen = () => setInternalMobileOpen(true); const location = useLocation(); const navigate = useNavigate(); const { user: currentUser, loading } = useUser(); @@ -30,6 +38,23 @@ export default function Sidebar() { navigate('/login'); }; + // Cerrar menú móvil cuando se navega o cuando la pantalla es grande + useEffect(() => { + const handleResize = () => { + if (window.innerWidth >= 1024) { // lg breakpoint + handleMobileClose(); + } + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + // Cerrar menú móvil cuando cambia la ubicación + useEffect(() => { + handleMobileClose(); + }, [location.pathname]); + // El usuario y loading ahora vienen del contexto global // Definir todas las secciones @@ -196,35 +221,79 @@ export default function Sidebar() { .filter(Boolean); return ( -
- {/* Header - Logo y colapsar */} -
-
- {!isCollapsed && ( -
- {/* Logo de la organización */} -
- - - + <> + {/* Botón flotante para abrir menú en móvil - solo cuando se usa standalone */} + {!mobileOpen && isMobileOpen === undefined && ( + + )} + + {/* Overlay para móviles */} + {mobileOpen && ( +
+ )} + + {/* Sidebar */} +
+ {/* Header - Logo y colapsar */} +
+
+ {!isCollapsed && ( +
+ {/* Logo de la organización */} +
+ + + +
+

EFC Dashboard

-

EFC Dashboard

+ )} + + {/* Botones de control */} +
+ {/* Botón cerrar en móvil */} + + + {/* Botón colapsar en desktop */} +
- )} - +
-
{/* Navigation */}
)}
-
+
+ + ); +} + +// 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 ( + ); } diff --git a/src/pages/Admin.jsx b/src/pages/Admin.jsx index d5aa601..aa979f9 100644 --- a/src/pages/Admin.jsx +++ b/src/pages/Admin.jsx @@ -87,123 +87,124 @@ export default function Admin() { } return ( -
+
{/* Header + Estado del Sistema alineados horizontalmente */} -
+
{/* Header principal mejorado */} -
-
- +
+
-
-

- Panel de Administración +
+

+ Panel de Administración {services && ( - + {services.en_espera} en espera )}

-

+

{typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true' ? 'Dashboard principal para gestión de Expediente electrónico' : 'Dashboard principal para gestión de agencia aduanal'}

- {/* Efecto decorativo de fondo */} -
- - - - - - - - - + {/* Efectos decorativos de fondo modernos */} +
+
+
+
+
+
+ {/* Partículas flotantes */} +
+
+
+
{/* Animación personalizada para el icono y contador */}
{/* Estado del Sistema card a la derecha */} -
-
+
-
- +
+
-

Estado del Sistema

+

Estado del Sistema

-
-
- API Backend - +
+
+ API Backend + Conectado
-
- API Servicios - +
+ API Servicios + Conectado
-
- Última Actualización - +
+ Última Actualización + Hace 2 min
- {/* Efecto decorativo de fondo */} -
- - - - - - - - - + {/* Efecto decorativo de fondo modernizado */} +
+
{/* Animación personalizada para el icono */}
@@ -211,170 +212,290 @@ export default function Admin() {
{/* Stats Cards con datos de endpoints */} -
+
{/* Estados de servicios */} -
-
-
-
- - - +
+
+
+
+
+ + + +
+
+
+

Procesos en Espera

+

{services ? services.en_espera : '-'}

+

Total: {services ? services.procesos_filtrados : '-'}

-
-
-

Procesos en Espera

-

{services ? services.en_espera : '-'}

-

Total: {services ? services.procesos_filtrados : '-'}

-
-
-
-
- - - +
+
+
+
+
+ + + +
+
+
+

En Proceso

+

{services ? services.en_proceso : '-'}

+

Finalizados: {services ? services.finalizados : '-'}

-
-
-

En Proceso

-

{services ? services.en_proceso : '-'}

-

Finalizados: {services ? services.finalizados : '-'}

-
-
-
-
- - - +
+
+
+
+
+ + + +
+
+
+

Con Error

+

{services ? services.con_error : '-'}

+

Finalizados: {services ? services.finalizados : '-'}

-
-
-

Con Error

-

{services ? services.con_error : '-'}

-

Finalizados: {services ? services.finalizados : '-'}

{/* Descargas */} -
-
-
-
- - - +
+
+
+
+
+ + + +
+
+
+

Descargados 1 día

+

{downloads ? downloads.archivos_ultimas_1_dia : '-'}

+
+ 7 días: {downloads ? downloads.archivos_ultimos_7_dias : '-'} + | 30 días: {downloads ? downloads.archivos_ultimos_30_dias : '-'} +
-
-
-

Descargados 1 día

-

{downloads ? downloads.archivos_ultimas_1_dia : '-'}

-

7 días: {downloads ? downloads.archivos_ultimos_7_dias : '-'} | 30 días: {downloads ? downloads.archivos_ultimos_30_dias : '-'}

{/* Análisis de actividad de usuario */} {!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && !isGroup35 && ( -
-

Actividad de Usuarios

- {loading ? ( -
Cargando...
- ) : error ? ( -
{error}
- ) : userActivity ? ( -
-
-

Resumen de acciones

-
    - {Object.entries(userActivity.actions_count).map(([action, count]) => ( -
  • - {action} - {count} -
  • - ))} -
  • - Total actividades - {userActivity.actividades_filtradas} -
  • -
-
-
-

Top usuarios

-
    - {userActivity.top_users.map((user, idx) => ( -
  1. - {user.username} - {user.activity_count} -
  2. - ))} -
+
+
+
+
+ + +
+

Actividad de Usuarios

- ) : null} + {loading ? ( +
+
+ Cargando... +
+ ) : error ? ( +
{error}
+ ) : userActivity ? ( +
+
+

+
+ Resumen de acciones +

+
+ {Object.entries(userActivity.actions_count).map(([action, count]) => ( +
+ {action} + {count} +
+ ))} +
+ Total actividades + {userActivity.actividades_filtradas} +
+
+
+
+

+
+ Top usuarios +

+
+ {userActivity.top_users.map((user, idx) => ( +
+
+ {idx + 1} + {user.username} +
+ {user.activity_count} +
+ ))} +
+
+
+ ) : null} +
)} {/* Tabla de últimos documentos */} -
-

Últimos documentos agregados

- {loading ? ( -
Cargando...
- ) : error ? ( -
{error}
- ) : ( -
- - - - - - - - - - - {latestDocs.map(doc => ( - - - - - - - ))} - -
ArchivoPedimentoOrganizaciónFecha
{getFileName(doc.archivo)}{doc.pedimento}{doc.organizacion}{new Date(doc.created_at).toLocaleString('es-MX')}
+
+
+
+
+ + + +
+

Últimos documentos agregados

- )} + {loading ? ( +
+
+ Cargando... +
+ ) : error ? ( +
{error}
+ ) : ( + <> + {/* Vista de tabla para pantallas grandes */} +
+ + + + + + + + + + + {latestDocs.map((doc, index) => ( + + + + + + + ))} + +
ArchivoPedimentoOrganizaciónFecha
+
+
+ + + +
+
+
+ {getFileName(doc.archivo)} +
+
+
+
+ {doc.pedimento} + {doc.organizacion} + {new Date(doc.created_at).toLocaleString('es-MX', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +
+
+ + {/* Vista de tarjetas para pantallas pequeñas y medianas */} +
+ {latestDocs.map((doc, index) => ( +
+
+
+ + + +
+
+
+ {getFileName(doc.archivo)} +
+
+
+ Pedimento: + {doc.pedimento} +
+
+ Organización: + {doc.organizacion} +
+
+ Fecha: + + {new Date(doc.created_at).toLocaleString('es-MX', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} + +
+
+
+
+
+ ))} +
+ + )} +
diff --git a/src/pages/Documents.jsx b/src/pages/Documents.jsx index 2ee2dbd..8486520 100644 --- a/src/pages/Documents.jsx +++ b/src/pages/Documents.jsx @@ -267,35 +267,35 @@ export default function Documents() { // El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla return ( -
+
{/* Header mejorado y decorativo */}
-
- +
+
-
-

+
+

Documentos - {totalDocuments} + {totalDocuments}

-

Descarga los documentos de tus pedimentos.

+

Descarga los documentos de tus pedimentos.

{/* Efecto decorativo de fondo */} -
+
- - + + @@ -324,37 +324,37 @@ export default function Documents() { (showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '') } style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}> -
+
{/* Header de Documentos Relacionados arriba de los filtros */} -
-

+
+

Todos los Documentos

{/* Filtros de query parameters */} -
+
{/* Filtros avanzados */} -
+
{/* Pedimento Número */} -
+
setPedimentoNumeroFilter(e.target.value)} - placeholder="Buscar por número de pedimento..." - className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" + placeholder="Buscar por número..." + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 transition-all" />
{/* Extensión */} -
+
{/* Tipo de documento */} -
+
{/* Fecha de creación */} -
+
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" />
@@ -403,86 +403,218 @@ export default function Documents() { setShowSuccessModal(false)} message={success || 'Descarga exitosa'} /> {/* Botones de descarga */} {currentDocuments.length > 0 && ( -
+
)}
-
6 ? 'auto' : 'hidden', position: 'relative' }}> - - - - - - - - - - - - - - {/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */} - {loading ? ( - - - - ) : error ? ( - - - - ) : currentDocuments.length > 0 ? ( - <> - {currentDocuments.map(doc => ( - - - - - - - - - - ))} - {/* Rellenar con filas vacías si hay menos de 8 */} - {currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => ( - - - - ))} - - ) : ( - - - - )} - -
+ + + {/* Vista responsiva: tabla para desktop, cards para mobile */} + {/* Tabla para pantallas grandes */} +
+
6 ? 'auto' : 'hidden', position: 'relative' }}> + + + + + + + + + + + + + + {/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */} + {loading ? ( + + + + ) : error ? ( + + + + ) : currentDocuments.length > 0 ? ( + <> + {currentDocuments.map(doc => ( + + + + + + + + + + ))} + {/* Rellenar con filas vacías si hay menos de 8 */} + {currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => ( + + + + ))} + + ) : ( + + + + )} + +
+ { 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' }} + /> + PedimentoArchivoTipoTamañoExtensiónAcciones
+
+ Cargando documentos... +
+
+
+ Error: {error.message || 'Error al cargar documentos'} +
+
+ 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' }} + /> + {doc.pedimento_numero}{doc.archivo ? doc.archivo.split('/').pop() : ''}{ + (() => { + 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 || ''; + } + })() + }{doc.size}{doc.extension} + +
+  
+
+
+ + + +
+

No hay documentos

+

Aún no tienes documentos registrados.

+
+
+
+
+ + {/* Cards para pantallas pequeñas */} +
+ {loading ? ( +
+
+
+ Cargando documentos... +
+
+ ) : error ? ( +
+
+
+ + + +
+ Error: {error.message || 'Error al cargar documentos'} +
+
+ ) : currentDocuments.length > 0 ? ( +
+ {/* Selección múltiple en mobile */} +
+
PedimentoArchivoTipoTamañoExtensiónAcciones
-
- Cargando documentos... + Seleccionar todos + + {selectedDocs.length} seleccionados +
+ {currentDocuments.map(doc => ( +
+
+ handleSelectOne(doc.id)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-0.5 flex-shrink-0" + /> +
+

+ Pedimento: {doc.pedimento_numero} +

+

+ {doc.archivo ? doc.archivo.split('/').pop() : ''} +

-
-
- Error: {error.message || 'Error al cargar documentos'} -
-
- 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' }} - /> - {doc.pedimento_numero}{doc.archivo ? doc.archivo.split('/').pop() : ''}{ + + +
+
+ Tipo: +

{ (() => { switch (String(doc.document_type)) { case '1': return 'Pedimento Partida'; @@ -494,150 +626,142 @@ export default function Documents() { default: return doc.document_type || ''; } })() - }

{doc.size}{doc.extension} - -
-  
-
-
- - - -
-

No hay pedimentos

-

Aún no tienes pedimentos registrados.

+ }

-
+
+ Tamaño: +

{doc.size}

+
+
+ Extensión: +

{doc.extension}

+
+
+ +
+ +
+
+ ))} +
+ ) : ( +
+
+ + + +
+

No hay documentos

+

Aún no tienes documentos registrados.

+
+ )}
{/* Botón de actualizar eliminado por solicitud */} setShowSuccessModal(false)} message={success || 'Descarga exitosa'} /> -
- -
{/* Paginación con botones numerados y elipsis */} {totalDocuments > 0 && (
- {(() => { - 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 ( -
-
- - -
-
+
+
+ + +
+
+ + + {(() => { + 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 => ( - - {pageNumbers.map(num => ( - - ))} - - - Página {currentPage} de {totalPages} -
-
- ); - })()} + )); + })()} + + + + Página {currentPage} de {Math.ceil(totalDocuments / itemsPerPage)} + +
+
)} -

diff --git a/src/pages/Expedientes.jsx b/src/pages/Expedientes.jsx index bf3e33c..ed20f21 100644 --- a/src/pages/Expedientes.jsx +++ b/src/pages/Expedientes.jsx @@ -160,326 +160,349 @@ export default function Documents() { // El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla return ( -
+
{/* Header mejorado y decorativo */}
-
- +
+
-
-

- Expedientes - {totalDocuments} +
+

+ Expedientes + {totalDocuments > 0 && ( + + {totalDocuments} registros + + )}

-

Gestiona y descarga los documentos de tus pedimentos.

+

Gestiona y descarga los documentos de tus pedimentos

- {/* Efecto decorativo de fondo */} -
- - - - - - - - - + {/* Efectos decorativos de fondo modernos */} +
+
+
+
+
+
+ {/* Partículas flotantes */} +
+
+
+
{/* Animación personalizada para el icono y contador */}
-
+
{/* Filtros avanzados */} -
- {/* Search global */} -
- - setSearchFilter(e.target.value)} - placeholder="Buscar pedimento, contribuyente, agente aduanal..." - className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
- {/* Pedimento */} -
- - setPedimentoFilter(e.target.value)} - placeholder="Buscar pedimento..." - className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
- {/* Alerta */} -
- - -
- {/* Expediente */} -
- - -
- {/* Contribuyente combobox */} -
- - { - setContribuyenteInput(e.target.value); - setContribuyenteFilter(''); - }} - placeholder="Buscar o escribir..." - className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - autoComplete="off" - /> - {/* Dropdown de sugerencias */} - {contribuyenteInput && ( -
- {contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? ( -
Sin coincidencias
- ) : ( - contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => ( - - )) - )} -
- )} -
- {/* CURP Apoderado */} -
- - setCurpApoderadoFilter(e.target.value)} - placeholder="CURP del apoderado..." - className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
- {/* Fecha de pago */} -
- - setFechaPagoFilter(e.target.value)} - className="w-44 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" /> -
- {/* Patente */} -
- - setPatenteFilter(e.target.value)} - placeholder="Patente..." - className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
- {/* Aduana */} -
- - setAduanaFilter(e.target.value)} - placeholder="Aduana..." - className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
- {/* Tipo de operación */} -
- - setTipoOperacionFilter(e.target.value)} - placeholder="ID tipo operación..." - className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
- {/* Clave pedimento */} -
- - setClavePedimentoFilter(e.target.value)} - placeholder="Clave pedimento..." - className="w-36 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> +
+

+ + + + Filtros de búsqueda +

+
+ {/* Search global */} +
+ + setSearchFilter(e.target.value)} + placeholder="Buscar pedimento, contribuyente..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Pedimento */} +
+ + setPedimentoFilter(e.target.value)} + placeholder="Número de pedimento..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Expediente */} +
+ + +
+ {/* Contribuyente combobox */} +
+ + { + setContribuyenteInput(e.target.value); + setContribuyenteFilter(''); + }} + placeholder="Buscar o escribir..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + autoComplete="off" + /> + {/* Dropdown de sugerencias */} + {contribuyenteInput && ( +
+ {contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? ( +
Sin coincidencias
+ ) : ( + contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => ( + + )) + )} +
+ )} +
+ {/* CURP Apoderado */} +
+ + setCurpApoderadoFilter(e.target.value)} + placeholder="CURP del apoderado..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Fecha de pago */} +
+ + setFechaPagoFilter(e.target.value)} + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" /> +
+ {/* Patente */} +
+ + setPatenteFilter(e.target.value)} + placeholder="Patente..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Aduana */} +
+ + setAduanaFilter(e.target.value)} + placeholder="Aduana..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Tipo de operación */} +
+ + setTipoOperacionFilter(e.target.value)} + placeholder="ID tipo operación..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+ {/* Clave pedimento */} +
+ + setClavePedimentoFilter(e.target.value)} + placeholder="Clave pedimento..." + className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
-
- - 🔄 Actualización automática cada 30 segundos - +
+
+ + + + + Actualización automática cada 30s + + {loading && ( + + + + + + Actualizando... + + )} +
-
{success && ( -
+
- - - -

{success}

+
+ + + +
+
+

{success}

+
)}
-
-
8 ? 'auto' : 'hidden', position: 'relative' }}> - - + {/* Vista de tabla para pantallas grandes */} +
+
+
+ - - - - - - - - - + + + + + + + + - - {/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */} + {loading ? ( - ) : error ? ( - ) : currentDocuments.length > 0 ? ( - <> - {currentDocuments.map(ped => ( - - - - - - - - - - - - ))} - {/* Rellenar con filas vacías si hay menos de 8 */} - {currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => ( - - - - ))} - + currentDocuments.map(ped => ( + + + + + + + + + + + )) ) : ( - @@ -488,9 +511,97 @@ export default function Documents() {
PedimentoFecha de pagoContribuyenteAlertaCURP ApoderadoImporte totalSaldo disponibleImporte pedimentoExpedientePedimentoFecha de pagoContribuyenteCURP ApoderadoImporte totalSaldo disponibleImporte pedimentoExpediente
-
- Cargando documentos... +
+
+
+ Cargando expedientes...
-
- Error: {error.message || 'Error al cargar documentos'} +
+
+
+ + + +
+ Error: {error.message || 'Error al cargar expedientes'}
- - {ped.pedimento} - - {ped.fechapago}{ped.contribuyente} - - {ped.alerta ? 'Sí' : 'No'} - - {ped.curp_apoderado}${ped.importe_total}${ped.saldo_disponible}${ped.importe_pedimento} - - {ped.existe_expediente ? 'Sí' : 'No'} - -
 
+ + {ped.pedimento} + + {ped.fechapago}{ped.contribuyente}{ped.curp_apoderado}${ped.importe_total}${ped.saldo_disponible}${ped.importe_pedimento} + + {ped.existe_expediente ? ( + <> + + + + Sí + + ) : ( + <> + + + + No + + )} + +
-
-
+
+
+
-

No hay pedimentos

-

Aún no tienes pedimentos registrados.

+

No hay expedientes

+

No se encontraron expedientes con los filtros aplicados.

- {/* Paginación con botones numerados y elipsis */} + + {/* Vista de tarjetas para pantallas pequeñas y medianas */} +
+ {loading ? ( +
+
+ Cargando expedientes... +
+ ) : error ? ( +
+
+ + + +
+ Error: {error.message || 'Error al cargar expedientes'} +
+ ) : currentDocuments.length > 0 ? ( + currentDocuments.map(ped => ( +
+
+
+
+ + + +
+
+ + {ped.pedimento} + +

{ped.fechapago}

+
+
+ + {ped.existe_expediente ? 'Con expediente' : 'Sin expediente'} + +
+ +
+
+ Contribuyente: + + {ped.contribuyente} + +
+ {ped.curp_apoderado && ( +
+ CURP Apoderado: + {ped.curp_apoderado} +
+ )} +
+
+ Importe total: + ${ped.importe_total} +
+
+ Saldo disponible: + ${ped.saldo_disponible} +
+
+ Importe pedimento: + ${ped.importe_pedimento} +
+
+
+
+ )) + ) : ( +
+
+ + + +
+

No hay expedientes disponibles

+

Intenta ajustar los filtros de búsqueda

+
+ )} +
+ {/* Paginación moderna y responsiva */} {totalDocuments > 0 && ( -
+
{(() => { const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage)); const maxPagesToShow = 5; @@ -505,26 +616,26 @@ export default function Documents() { pageNumbers.push(i); } return ( -
-
- +
+
+
-
+
@@ -532,26 +643,33 @@ export default function Documents() { type="button" onClick={e => handlePageChange(currentPage - 1, e)} disabled={currentPage === 1} - className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`} + className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${currentPage === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`} > ‹ - {pageNumbers.map(num => ( - - ))} +
+ {pageNumbers.map(num => ( + + ))} +
+
+ + {currentPage} / {totalPages} + +
@@ -559,11 +677,15 @@ export default function Documents() { type="button" onClick={e => handlePageChange(totalPages, e)} disabled={currentPage >= totalPages} - className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`} + className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`} > » - Página {currentPage} de {totalPages} +
+
+ + Mostrando {((currentPage - 1) * itemsPerPage) + 1} a {Math.min(currentPage * itemsPerPage, totalDocuments)} de {totalDocuments} registros +
); diff --git a/src/pages/Organization.jsx b/src/pages/Organization.jsx index 7c96155..f3c0e57 100644 --- a/src/pages/Organization.jsx +++ b/src/pages/Organization.jsx @@ -67,202 +67,294 @@ export default function Organization() { }, [info]); if (loading) return ( -
-
- - - - -

Cargando información de la organización...

+
+
+
+ + + + +
+
+

Cargando información

+

Obteniendo datos de la organización...

); if (error) return ( -
-
-
- - - -

{error}

+
+
+
+
+
+ + + +
+

Error al cargar

+

{error}

); return ( -
-
+
+
{/* Header mejorado y decorativo */} -
-
- +
+
+
-
-

- Mi Organización +
+

+ Mi Organización {info && ( - + {info.total_usuarios} usuarios )}

-

Información y métricas de uso de tu organización

+

Información y métricas de uso de tu organización

- {/* Efecto decorativo de fondo */} -
- - - - - - - - - + {/* Efectos decorativos de fondo modernos */} +
+
+
+
+
+
+ {/* Partículas flotantes */} +
+
+
+
{/* Animación personalizada para el icono y contador */}
{/* Barra de almacenamiento con color y progress bar */} -
-

- - - - Uso de Almacenamiento -

-
- {/* 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 ( -
- ); - })()} - {/* Etiquetas sobre la barra */} -
- - - {info?.espacio_utilizado_gb?.toFixed(2)} GB usados - - {info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB libres - {info?.limite_almacenamiento_gb} GB límite +
+
+
+
+

+
+ + + +
+ Uso de Almacenamiento +

+ + {/* Estadísticas rápidas */} +
+
+
+
+ Usado +
+
+ {info?.espacio_utilizado_gb?.toFixed(2)} GB +
+
+
+
+
+ Disponible +
+
+ {info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB +
+
+
+
+
+ Límite +
+
+ {info?.limite_almacenamiento_gb} GB +
+
+
+ + {/* Barra de progreso mejorada */} +
+ {/* 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 ( +
+ {animatedPercent > 20 && ( + + {animatedPercent.toFixed(1)}% + + )} +
+ ); + })()} + {/* Indicador de porcentaje fuera de la barra si es muy pequeña */} + {animatedPercent <= 20 && ( +
+ + {animatedPercent.toFixed(1)}% + +
+ )} +
-
+
{/* Tarjeta Organización */} -
-
- - - +
+
+
+
+ + + +
+ Organización + {info?.organizacion}
- Organización - {info?.organizacion}
+ {/* Tarjeta Usuarios */} -
-
- - - +
+
+
+
+ + + +
+ Usuarios + {info?.total_usuarios}
- Usuarios - {info?.total_usuarios}
+ {/* Tarjeta Pedimentos */} -
-
- - - +
+
+
+
+ + + +
+ Pedimentos + {info?.total_pedimentos}
- Pedimentos - {info?.total_pedimentos}
+ {/* Tarjeta Documentos */} -
-
- - - +
+
+
+
+ + + +
+ Documentos + {info?.total_documentos}
- Documentos - {info?.total_documentos}
{/* Tarjeta Límite de Almacenamiento */} -
-
- - - +
+
+
+
+ + + +
+ Límite de Almacenamiento + {info?.limite_almacenamiento_gb} GB
- Límite de Almacenamiento - {info?.limite_almacenamiento_gb} GB
+ {/* Tarjeta Espacio Utilizado */} -
-
- - - +
+
+
+
+ + + +
+ Espacio Utilizado + {info?.espacio_utilizado_gb?.toFixed(2)} GB
- Espacio Utilizado - {info?.espacio_utilizado_gb?.toFixed(2)} GB
+ {/* Tarjeta Espacio Disponible */} -
-
- - - +
+
+
+
+ + + +
+ Espacio Disponible + {info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB
- Espacio Disponible - {info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB
+ {/* Tarjeta Porcentaje Utilizado */} -
-
- - - - +
+
+
+
+ + + + +
+ Porcentaje Utilizado + {info?.porcentaje_utilizado}%
- Porcentaje Utilizado - {info?.porcentaje_utilizado}%
diff --git a/src/pages/Procesos.jsx b/src/pages/Procesos.jsx index 4282868..930baba 100644 --- a/src/pages/Procesos.jsx +++ b/src/pages/Procesos.jsx @@ -64,9 +64,14 @@ export default function Procesos() { } try { const token = localStorage.getItem('access'); + if (!token) { + alert('No hay token de autenticación. Por favor, inicia sesión nuevamente.'); + setExecutingId(null); + return; + } const headers = { 'Content-Type': 'application/json', - ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + 'Authorization': `Bearer ${token}`, }; const body = JSON.stringify({ pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, @@ -90,14 +95,19 @@ export default function Procesos() { // Cierra el dropdown si se hace click fuera useEffect(() => { if (openDropdownId === null) return; - function handleClick(e) { + function handleClickOutside(e) { const el = document.getElementById(`dropdown-acciones-${openDropdownId}`); if (el && !el.contains(e.target)) { setOpenDropdownId(null); } } - document.addEventListener('mousedown', handleClick); - return () => document.removeEventListener('mousedown', handleClick); + // Usar setTimeout para evitar que el click que abre el dropdown lo cierre inmediatamente + setTimeout(() => { + document.addEventListener('click', handleClickOutside); + }, 100); + return () => { + document.removeEventListener('click', handleClickOutside); + }; }, [openDropdownId]); useEffect(() => { @@ -106,6 +116,10 @@ export default function Procesos() { setError(''); try { const token = localStorage.getItem('access'); + if (!token) { + setError('No hay token de autenticación. Por favor, inicia sesión nuevamente.'); + return; + } // Construir query params const params = new URLSearchParams(); params.append('page', String(page)); @@ -117,10 +131,21 @@ export default function Procesos() { params.append('ordering', (sortOrder === 'desc' ? '-' : '') + sortField); } const API_URL = import.meta.env.VITE_EFC_API_URL; - const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + const headers = { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + console.log('Fetching procesos with token:', token ? 'Token present' : 'No token'); + console.log('URL:', `${API_URL}/customs/procesamientopedimentos/?${params.toString()}`); const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`, { headers }); - if (!res.ok) throw new Error('Error al obtener procesamiento de pedimentos'); + console.log('Response status:', res.status); + if (!res.ok) { + const errorText = await res.text(); + console.log('Error response:', errorText); + throw new Error(`Error al obtener procesamiento de pedimentos: ${res.status} - ${errorText}`); + } const data = await res.json(); + console.log('Data received:', data); setProcesos(data.results || []); setCount(data.count || 0); } catch (err) { @@ -133,41 +158,48 @@ export default function Procesos() { }, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]); return ( -
+
-
-
- + {/* Header mejorado y responsivo */} +
+
+
-
-

- Procesos del Sistema - - {count} - +
+

+ Procesos del Sistema + {count > 0 && ( + + {count} procesos + + )}

-

Estado actual de los procesos de la agencia aduanal

+

Estado actual de los procesos de la agencia aduanal

-
- - - - - - - - - + {/* Efectos decorativos de fondo modernos */} +
+
+
+
+
+ {/* Partículas flotantes */} +
+
+
+
+
+ {/* Animaciones CSS */}
-
-
-

Procesamiento de Pedimentos

+ {/* Contenido principal */} +
+
+

+
+ + + +
+ Procesamiento de Pedimentos +

+ {count > 0 && ( +
+ Total de registros: + {count} +
+ )}
- {/* Filtros */} -
-
- - setPedimentoPedimentoFilter(e.target.value)} - placeholder="Buscar por pedimento..." - className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50" - /> -
-
- - -
-
- - + + {/* Filtros responsivos mejorados */} +
+

+ + + + Filtros de búsqueda +

+
+
+ + setPedimentoPedimentoFilter(e.target.value)} + placeholder="Buscar por pedimento..." + className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md" + /> +
+
+ + +
+
+ + +
+ {/* Estados de carga y error mejorados */} {loading ? ( -
Cargando procesos...
+
+
+
+
+
+

Cargando procesos...

+
) : error ? ( -
{error}
+
+
+ + + +
+

Error al cargar

+

{error}

+
) : ( -
-
- - + <> + {/* Vista de tabla para pantallas grandes */} +
+
+ - - - - - - - - {procesos.length === 0 ? ( - - - - ) : ( - procesos.map((proc) => ( - - - - - - - + {procesos.length === 0 ? ( + + - )) - )} - + ) : ( + procesos.map((proc) => ( + + + + + + + + + )) + )} +
{ setSortField('id'); setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc'); }} > - ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')} +
+ ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')} +
{ setSortField('organizacion_name'); setSortOrder(sortField === 'organizacion_name' && sortOrder === 'asc' ? 'desc' : 'asc'); }} > - Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')} +
+ Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')} +
{ setSortField('estado'); setSortOrder(sortField === 'estado' && sortOrder === 'asc' ? 'desc' : 'asc'); }} > - Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')} +
+ Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')} +
{ setSortField('pedimento'); setSortOrder(sortField === 'pedimento' && sortOrder === 'asc' ? 'desc' : 'asc'); }} > - Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')} +
+ Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')} +
{ setSortField('servicio'); setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc'); }} > - Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')} +
+ Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')} +
+ Acciones
No hay registros
{proc.id}{proc.organizacion_name || '-'}{ - proc.estado === 1 ? 'En Espera' - : proc.estado === 2 ? 'Procesando' - : proc.estado === 3 ? 'Finalizado' - : proc.estado === 4 ? 'Error' - : String(proc.estado) - }{ - typeof proc.pedimento === 'object' && proc.pedimento !== null - ? proc.pedimento.pedimento || JSON.stringify(proc.pedimento) - : proc.pedimento - }{ - 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) - } -
- - {openDropdownId === proc.id && ( -
-
- - - -
-
- )} +
+
+
+ + + +
+

No hay procesos disponibles

+

Intenta ajustar los filtros de búsqueda

+ {proc.id} + {proc.organizacion_name || '-'} + {(() => { + 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 ( + + {estado.text} + + ); + })()} + + {typeof proc.pedimento === 'object' && proc.pedimento !== null + ? proc.pedimento.pedimento || JSON.stringify(proc.pedimento) + : proc.pedimento} + + {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)} + +
+ + {openDropdownId === proc.id && ( +
+
+ + + +
+
+ )} +
+
- {/* Paginación igual a Documents.jsx */} + + {/* Vista de tarjetas para pantallas pequeñas y medianas */} +
+ {procesos.length === 0 ? ( +
+
+ + + +
+

No hay procesos disponibles

+

Intenta ajustar los filtros de búsqueda

+
+ ) : ( + procesos.map((proc) => ( +
+
+
+
+ + + +
+
+

Proceso #{proc.id}

+

{proc.organizacion_name || 'Sin organización'}

+
+
+ {(() => { + 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 ( + + {estado.text} + + ); + })()} +
+ +
+
+ Pedimento: + + {typeof proc.pedimento === 'object' && proc.pedimento !== null + ? proc.pedimento.pedimento || JSON.stringify(proc.pedimento) + : proc.pedimento} + +
+
+ Servicio: + + {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)} + +
+
+ +
+ + {openDropdownId === proc.id && ( +
+
+ + + +
+
+ )} +
+
+ )) + )} +
+ + {/* Paginación compartida mejorada */} {count > 0 && ( -
+
{(() => { const totalPages = Math.max(1, Math.ceil(count / itemsPerPage)); const maxPagesToShow = 5; @@ -385,26 +657,26 @@ export default function Procesos() { pageNumbers.push(i); } return ( -
-
- + <> +
+
-
+
@@ -412,7 +684,7 @@ export default function Procesos() { type="button" onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }} disabled={page === 1} - className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`} + className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`} > ‹ @@ -421,7 +693,7 @@ export default function Procesos() { type="button" key={num} onClick={e => { e.preventDefault(); setPage(num); }} - className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === page ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`} + className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${num === page ? 'bg-blue-600 text-white border-blue-700 cursor-default shadow-lg' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`} disabled={num === page} > {num} @@ -431,7 +703,7 @@ export default function Procesos() { type="button" onClick={e => { e.preventDefault(); setPage(p => p + 1); }} disabled={page >= totalPages} - className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`} + className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`} > › @@ -439,18 +711,20 @@ export default function Procesos() { type="button" onClick={e => { e.preventDefault(); setPage(totalPages); }} disabled={page >= totalPages} - className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`} + className={`px-3 py-2 rounded-xl border text-sm font-semibold transition-all duration-200 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 hover:shadow-md'}`} > » - Página {page} de {totalPages}
-
+ + Página {page} de {totalPages} + + ); })()}
)} -
+ )}
diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index 7cfdfaf..fd172de 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -1,10 +1,53 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useLayoutEffect, useRef } from 'react'; import { getCurrentUser } from '../api/users.ts'; +import { useNotification } from '../context/NotificationContext'; + +// Animación fade-in/slide-up para el componente +const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`; +if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-settings')) { + const style = document.createElement('style'); + style.id = 'fadein-slideup-settings'; + style.innerHTML = fadeInSlideUp; + document.head.appendChild(style); +} const Settings = () => { const [activeTab, setActiveTab] = useState('profile'); const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const { showMessage } = useNotification(); + + // Estados para el formulario de perfil + const [formData, setFormData] = useState({ + first_name: '', + last_name: '', + email: '', + username: '', + rfc: '' + }); + + // Estado para controlar la animación de entrada + const [showAnimation, setShowAnimation] = useState(false); + const [hasAnimated, setHasAnimated] = useState(false); + + // Ref para el input de archivo + const fileInputRef = useRef(null); + + useLayoutEffect(() => { + // Forzar un render antes de activar la animación + setShowAnimation(true); + }, []); + + useEffect(() => { + if (showAnimation && !hasAnimated) { + const timeout = setTimeout(() => { + setHasAnimated(true); + setShowAnimation(false); + }, 700); // Duración igual a la animación + return () => clearTimeout(timeout); + } + }, [showAnimation, hasAnimated]); // Cargar información del usuario al montar el componente useEffect(() => { @@ -14,16 +57,198 @@ const Settings = () => { if (token) { const userData = await getCurrentUser(token); setCurrentUser(userData); + // Inicializar formData con los datos del usuario + setFormData({ + first_name: userData.first_name || '', + last_name: userData.last_name || '', + email: userData.email || '', + username: userData.username || '', + rfc: userData.rfc || '' + }); } } catch (error) { console.error('Error al cargar datos del usuario:', error); + showMessage('Error al cargar los datos del usuario', 'error'); } finally { setLoading(false); } }; loadUserData(); - }, []); + }, [showMessage]); + + // Función para actualizar el usuario + const updateUser = async (userData) => { + const token = localStorage.getItem('access'); + const API_URL = import.meta.env.VITE_EFC_API_URL; + + try { + const response = await fetch(`${API_URL}/user/users/${currentUser.id}/`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData), + }); + + if (response.status === 401) { + showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); + localStorage.removeItem('access'); + localStorage.removeItem('refresh'); + setTimeout(() => { + window.location.href = '/login'; + }, 2000); + return null; + } + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Error al actualizar el usuario'); + } + + return await response.json(); + } catch (error) { + console.error('Error updating user:', error); + throw error; + } + }; + + // Manejar cambios en el formulario + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + // Guardar cambios del perfil + const handleSaveProfile = async () => { + if (!currentUser) return; + + // Validaciones básicas + if (!formData.first_name.trim()) { + showMessage('El nombre es requerido', 'error'); + return; + } + if (!formData.last_name.trim()) { + showMessage('El apellido es requerido', 'error'); + return; + } + if (!formData.email.trim()) { + showMessage('El email es requerido', 'error'); + return; + } + // Username validation removed - field is now read-only + + // Validar formato de email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + showMessage('Por favor ingresa un email válido', 'error'); + return; + } + + setSaving(true); + try { + // Preparar datos para enviar - NO incluir grupos para preservarlos + const updateData = { + first_name: formData.first_name.trim(), + last_name: formData.last_name.trim(), + email: formData.email.trim(), + // Username NO se incluye - no se puede modificar + ...(formData.rfc.trim() && { rfc: formData.rfc.trim() }), // Solo incluir RFC si existe + is_importador: currentUser.is_importador, // Preservar estado actual + is_active: currentUser.is_active // Preservar estado actual + }; + + const updatedUser = await updateUser(updateData); + if (updatedUser) { + setCurrentUser(updatedUser); + showMessage('Perfil actualizado exitosamente', 'success'); + } + } catch (error) { + showMessage(error.message || 'Error al actualizar el perfil', 'error'); + } finally { + setSaving(false); + } + }; + + // Manejar cambio de foto + const handlePhotoChange = async (event) => { + const file = event.target.files[0]; + if (!file) return; + + // Validar tipo de archivo + if (!file.type.startsWith('image/')) { + showMessage('Por favor selecciona un archivo de imagen válido', 'error'); + return; + } + + // Validar tamaño (5MB máximo) + if (file.size > 5 * 1024 * 1024) { + showMessage('La imagen debe ser menor a 5MB', 'error'); + return; + } + + setSaving(true); + try { + const token = localStorage.getItem('access'); + const API_URL = import.meta.env.VITE_EFC_API_URL; + + const formDataPhoto = new FormData(); + formDataPhoto.append('profile_picture', file); + + const response = await fetch(`${API_URL}/user/users/${currentUser.id}/`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + }, + body: formDataPhoto, + }); + + if (response.status === 401) { + showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); + localStorage.removeItem('access'); + localStorage.removeItem('refresh'); + setTimeout(() => { + window.location.href = '/login'; + }, 2000); + return; + } + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || 'Error al actualizar la foto'); + } + + const updatedUser = await response.json(); + setCurrentUser(updatedUser); + showMessage('Foto de perfil actualizada exitosamente', 'success'); + } catch (error) { + console.error('Error updating photo:', error); + showMessage(error.message || 'Error al actualizar la foto', 'error'); + } finally { + setSaving(false); + } + }; + + // Función para abrir selector de archivos + const handlePhotoClick = () => { + fileInputRef.current?.click(); + }; + + // Verificar si hay cambios en el formulario + const hasChanges = () => { + if (!currentUser) return false; + return ( + formData.first_name !== (currentUser.first_name || '') || + formData.last_name !== (currentUser.last_name || '') || + formData.email !== (currentUser.email || '') || + // Username excluded from change detection - field is read-only + formData.rfc !== (currentUser.rfc || '') + ); + }; // Solo mostrar tabs permitidas si es importador const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'; @@ -65,151 +290,231 @@ const Settings = () => { }; const renderProfileTab = () => ( -
+
-

Información Personal

+
+

Información Personal

+
+ Perfil +
+
{loading ? ( -
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
) : ( <> - {/* Avatar y información básica */} -
-
- {currentUser?.profile_picture ? ( - Avatar del usuario +
+
+ {currentUser?.profile_picture ? ( + Avatar del usuario + ) : ( +
+ + + +
+ )} +
+
+

+ {currentUser ? `${currentUser.first_name} ${currentUser.last_name}` : 'Usuario'} +

+

+ @{currentUser?.username || 'usuario'} +

+

+ ID: {currentUser?.id || 'Sin ID'} +

+ + {/* Input oculto para seleccionar archivo */} + - ) : ( -
- - - -
- )} -
-
-

- {currentUser ? `${currentUser.first_name} ${currentUser.last_name}` : 'Usuario'} -

-

- {currentUser?.username || 'Sin username'} -

-

- ID: {currentUser?.id || 'Sin ID'} -

- +
- {/* Formulario de información */} -
-
-