import React, { useEffect, useState, useRef } from 'react'; import { Link } from 'react-router-dom'; import { fetchTasks, ejecutarComando } from '../api/procesos.ts'; import { fetchWithAuth } from '../fetchWithAuth'; import { useNotification } from '../context/NotificationContext'; const API_URL = import.meta.env.VITE_EFC_API_URL; // Modal para mostrar detalles del task const TaskDetailsModal = ({ task, onClose }) => { if (!task) return null; const getStatusColor = (status) => { switch (status?.toUpperCase()) { case 'SUCCESS': return 'bg-green-100 text-green-800 border-green-200'; case 'PENDING': return 'bg-yellow-100 text-yellow-800 border-yellow-200'; case 'RUNNING': return 'bg-blue-100 text-blue-800 border-blue-200'; case 'FAILED': case 'FAILURE': return 'bg-red-100 text-red-800 border-red-200'; default: return 'bg-gray-100 text-gray-800 border-gray-200'; } }; // Función para parsear el error si es un string JSON const parseError = (errorStr) => { try { if (typeof errorStr === 'string') { // Intentar extraer el objeto detail del error const match = errorStr.match(/detail=({.*?})\)/); if (match && match[1]) { return JSON.parse(match[1].replace(/'/g, '"')); } return JSON.parse(errorStr); } return errorStr; } catch (e) { return { message: errorStr }; } }; return (

Detalles de la Tarea

{/* Información básica de la tarea */}

Task ID

{task.task_id}

Estado

{task.status}

Fecha

{new Date(task.timestamp).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true })}

Progreso

{task.progress || 0}%

{/* Mensajes y Errores */}

Mensaje de la tarea

{(() => { // Intentar parsear el mensaje si contiene un error HTTPException if (task.message && task.message.includes('HTTPException')) { try { const match = task.message.match(/detail=({.*?})\)/); if (match && match[1]) { const detail = JSON.parse(match[1].replace(/'/g, '"')); return (

{detail.message}

{detail.errors && detail.errors.length > 0 && (
    {detail.errors.map((error, idx) => (
  • {error}
  • ))}
)} {detail.data && (
Archivo de Error:

{detail.data.error_file}

)} {detail.metadata && (
Información Adicional:
{Object.entries(detail.metadata).map(([key, value]) => (
{key}: {value}
))}
)}
); } } catch (e) { console.error('Error parsing message:', e); } } // Si no se puede parsear, mostrar el mensaje original return

{task.message}

; })()} {/* Mostrar detalles de error si existe */} {(task.status === 'FAILURE' || task.status === 'FAILED') && task.error && (

Detalles del Error

{(() => { const errorDetail = parseError(task.error); if (errorDetail.detail) { return (
{/* Mensaje principal del error */} {errorDetail.detail.message && (

{errorDetail.detail.message}

)} {/* Lista de errores específicos */} {errorDetail.detail.errors && errorDetail.detail.errors.length > 0 && (
Errores detectados:
    {errorDetail.detail.errors.map((error, idx) => (
  • {error}
  • ))}
)} {/* Datos adicionales del error */} {errorDetail.detail.data && (
Archivos relacionados:
{Object.entries(errorDetail.detail.data).map(([key, value]) => (
{key}: {value}
))}
)} {/* Metadata */} {errorDetail.detail.metadata && (
Metadata:
{Object.entries(errorDetail.detail.metadata).map(([key, value]) => (
{key}: {value}
))}
)}
); } return (

{task.error}

); })()}
)} {/* Mensaje del resultado si existe */} {task.result?.message && ( <>

Mensaje del resultado

{task.result.message}

)}
{/* Detalles del resultado */} {task.result?.data && (

Detalles del Resultado

{/* Información del documento si existe */} {task.result.data.data?.documento && (
Información del Documento

Número de Pedimento

{task.result.data.data.documento.pedimento_numero}

Tipo de Documento

{task.result.data.data.documento.document_type}

Tamaño

{task.result.data.data.documento.size.toLocaleString()} bytes

Extensión

{task.result.data.data.documento.extension}

)} {/* Información de la partida si existe */} {task.result.data.data?.partida_update_response && (
Información de la Partida

Número de Partida

{task.result.data.data.partida_update_response.numero_partida}

Estado de Descarga

{task.result.data.data.partida_update_response.descargado ? 'Descargado' : 'Pendiente'}
)} {/* Metadata si existe */} {task.result.data?.metadata && (
Metadata
{Object.entries(task.result.data.metadata).map(([key, value]) => (

{key}

{value}

))}
)}
)}
); }; export default function Procesos() { const { showMessage } = useNotification(); const [procesos, setProcesos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [page, setPage] = useState(1); const [count, setCount] = useState(0); const [itemsPerPage, setItemsPerPage] = useState(12); const [selectedTask, setSelectedTask] = useState(null); const [loadingTask, setLoadingTask] = useState(false); const handleTaskClick = async (taskId) => { try { setLoadingTask(true); const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL_2; const response = await fetchWithAuth(`${MICROSERVICE_URL}/async/task-status/${taskId}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); // Si hay un error, aún queremos mostrar el modal con la información disponible setSelectedTask({ task_id: taskId, status: 'FAILURE', message: 'Error al obtener detalles del task', error: errorData.detail || errorData.message || 'Error en la respuesta del servidor', timestamp: new Date().toISOString() }); return; // Salimos pero ya hemos establecido el selectedTask } const data = await response.json(); setSelectedTask(data); } catch (error) { console.error('Error al obtener detalles del task:', error); // En caso de error, mostramos el modal con la información del error setSelectedTask({ task_id: taskId, status: 'FAILURE', message: 'Error al obtener detalles del task', error: error.message || 'Error desconocido', timestamp: new Date().toISOString() }); } finally { setLoadingTask(false); } }; const [pedimentoPedimentoFilter, setPedimentoPedimentoFilter] = useState(''); const [servicioFilter, setServicioFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [organizacionFilter, setOrganizacionFilter] = useState(''); const [organizaciones, setOrganizaciones] = useState([]); const [loadingOrganizaciones, setLoadingOrganizaciones] = useState(false); // Sorting const [sortField, setSortField] = useState(''); const [sortOrder, setSortOrder] = useState('asc'); // 'asc' | 'desc' // Ref para rastrear valores previos de filtros y detectar cambios const prevFiltersRef = useRef({ pedimentoPedimentoFilter: '', statusFilter: '', servicioFilter: '', organizacionFilter: '', // Añadir esta línea sortField: '', sortOrder: 'asc' }); // No se requieren estados ni funciones de acciones masivas useEffect(() => { async function fetchData() { // Detectar si algún filtro cambió const currentFilters = { pedimentoPedimentoFilter, servicioFilter, statusFilter, organizacionFilter, // Añadir esta línea sortField, sortOrder }; const filtersChanged = Object.keys(currentFilters).some( key => currentFilters[key] !== prevFiltersRef.current[key] ); // Si los filtros cambiaron y no estamos en la página 1, resetear página if (filtersChanged && page !== 1) { setPage(1); // Actualizar ref con valores actuales prevFiltersRef.current = { ...currentFilters }; return; // Salir temprano, el efecto se ejecutará de nuevo con page = 1 } // Actualizar ref con valores actuales prevFiltersRef.current = { ...currentFilters }; setLoading(true); setError(''); try { // Construir filtros const filters = {}; if (pedimentoPedimentoFilter) filters['pedimento_app'] = pedimentoPedimentoFilter; if (servicioFilter) filters['servicio'] = servicioFilter; if (statusFilter) filters['status'] = statusFilter; if (organizacionFilter) filters['organizacion'] = organizacionFilter; // Añadir esta línea if (sortField) { // Mapear campos antiguos a nuevos si es necesario const fieldMapping = { 'id': 'task_id', 'estado': 'status', // Agregar más mappings según sea necesario }; const mappedField = fieldMapping[sortField] || sortField; filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + mappedField; } const data = await fetchTasks(page, itemsPerPage, filters); setProcesos(data.results || []); setCount(data.count || 0); } catch (err) { if (err.message === 'SESSION_EXPIRED') { setError('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'); } else { setError(err instanceof Error ? err.message : String(err)); } } finally { setLoading(false); } } fetchData(); }, [page, itemsPerPage, pedimentoPedimentoFilter, servicioFilter, statusFilter, organizacionFilter, sortField, sortOrder]); const [showProcesosDropdown, setShowProcesosDropdown] = useState(false); const [ejecutandoProceso, setEjecutandoProceso] = useState(false); const handleEjecutarProcesamiento = async (params) => { // Verificar si se ha seleccionado una organización if (!organizacionFilter) { showMessage('Debes seleccionar una organización antes de ejecutar el proceso', 'warning'); return; // Detener la ejecución } try { setEjecutandoProceso(true); setShowProcesosDropdown(false); // Agregar el ID de la organización a los parámetros const paramsConOrganizacion = { ...params, organizacionid: organizacionFilter // Solo necesitamos el ID }; console.log('Ejecutando proceso con parámetros:', paramsConOrganizacion); const resultado = await ejecutarComando(paramsConOrganizacion); if (resultado.message) { // Mostrar mensaje de éxito showMessage(`${resultado.message}`, 'success'); // Recargar los datos después de 2 segundos setTimeout(() => { // Forzar recarga de datos const currentFilters = { pedimentoPedimentoFilter, servicioFilter, statusFilter, sortField, sortOrder }; prevFiltersRef.current = { ...currentFilters }; // Esto activará el useEffect para recargar setPage(prev => prev); }, 2000); } else if (resultado.error) { showMessage(`Error: ${resultado.error}`, 'error'); } } catch (error) { // console.error('Error al ejecutar procesamiento:', error); showMessage(`Error: ${error.message}`, 'error'); } finally { setEjecutandoProceso(false); } }; // Agrega este efecto para cerrar el dropdown useEffect(() => { const handleClickOutside = (event) => { if (showProcesosDropdown && !event.target.closest('.relative')) { setShowProcesosDropdown(false); } }; document.addEventListener('click', handleClickOutside); return () => { document.removeEventListener('click', handleClickOutside); }; }, [showProcesosDropdown]); useEffect(() => { async function fetchOrganizaciones() { try { setLoadingOrganizaciones(true); const response = await fetchWithAuth(`${API_URL}/organization/organizaciones/`); if (response.ok) { const data = await response.json(); setOrganizaciones(data.results || []); } } catch (error) { console.error('Error al cargar organizaciones:', error); } finally { setLoadingOrganizaciones(false); } } fetchOrganizaciones(); }, []); return (
{/* Modal de detalles del task */} {selectedTask && ( setSelectedTask(null)} /> )} {loadingTask && (
)}
{/* Header mejorado y responsivo */}

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

Estado actual de los procesos de la agencia aduanal

{/* Efectos decorativos de fondo modernos */}
{/* Partículas flotantes */}
{/* Animaciones CSS */}
{/* Contenido principal */}

Procesamiento de Pedimentos

{count > 0 && (
Total de registros: {count}
)}
{/* Filtros responsivos mejorados */}

Filtros de búsqueda

{ setPedimentoPedimentoFilter(e.target.value); setPage(1); }} placeholder="Buscar por pedimento..." className="w-full px-4 py-3 text-sm transition-all duration-200 bg-white border border-gray-300 shadow-sm rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 hover:shadow-md" />
{/* BOTÓN PARA EJECUTAR PROCESAMIENTOS - AGREGAR AQUÍ */}
{/* Dropdown de opciones de procesamiento */} {showProcesosDropdown && (
{/* Encabezado del dropdown */}

Selecciona un proceso

Se ejecutará para tu organización

{/* Opción "Todos" */}
{/* Opciones específicas */} {[ { id: 'procesamiento_pedimento', label: 'Procesamiento Inicial', desc: 'Procemiento Inicial de consulta a VU' }, { id: 'pedimentos_completos', label: 'Pedimento Completo', desc: 'Procesar pedimentos completos' }, { id: 'remesas', label: 'Remesas', desc: 'Procesar remesas' }, { id: 'partidas', label: 'Partidas', desc: 'Procesar partidas' }, { id: 'coves', label: 'Coves', desc: 'Procesar coves' }, { id: 'edocs', label: 'Edocuments', desc: 'Procesar edocuments' }, { id: 'acuse_coves', label: 'Acuses COVE', desc: 'Procesar acuses COVE' }, { id: 'acuses', label: 'Acuses', desc: 'Procesar acuses' } ].map((proceso) => ( ))}
)}
{/* Estados de carga y error mejorados */} {loading ? (

Cargando procesos...

) : error ? (

Error al cargar

{error}

) : ( <> {/* Vista de tabla para pantallas grandes */}
{procesos.length === 0 ? ( ) : ( procesos.map((proc) => ( )) )}
{ setSortField('task_id'); setSortOrder(sortField === 'task_id' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
Task ID {sortField === 'task_id' && (sortOrder === 'asc' ? '▲' : '▼')}
{ setSortField('pedimento_app'); setSortOrder(sortField === 'pedimento_app' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
Pedimento {sortField === 'pedimento_app' && (sortOrder === 'asc' ? '▲' : '▼')}
{ setSortField('status'); setSortOrder(sortField === 'status' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
Estado {sortField === 'status' && (sortOrder === 'asc' ? '▲' : '▼')}
{ setSortField('timestamp'); setSortOrder(sortField === 'timestamp' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
Fecha de creación {sortField === 'timestamp' && (sortOrder === 'asc' ? '▲' : '▼')}
{ setSortField('servicio'); setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')}

No hay procesos disponibles

Intenta ajustar los filtros de búsqueda

{proc.pedimento_app || '-'} {(() => { const estado = proc.status?.toLowerCase() === 'pending' ? { text: 'En Espera', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' } : proc.status?.toLowerCase() === 'running' ? { text: 'Procesando', color: 'bg-blue-100 text-blue-800 border-blue-200' } : proc.status?.toLowerCase() === 'completed' ? { text: 'Finalizado', color: 'bg-green-100 text-green-800 border-green-200' } : proc.status?.toLowerCase() === 'failed' || proc.status?.toLowerCase() === 'failure' ? { text: 'Error', color: 'bg-red-100 text-red-800 border-red-200' } : { text: String(proc.status), color: 'bg-gray-100 text-gray-800 border-gray-200' }; return ( {estado.text} ); })()} {new Date(proc.timestamp).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true })} {(() => { const services = { '1': 'Estado de pedimento', '2': 'Listado de pedimentos', '3': 'Pedimento Completo', '4': 'Pedimento Partidas', '5': 'Pedimento Remesas', '6': 'Acuse', '7': 'EDocument', '8': 'Cove', '9': 'Acuse Cove' }; return ( {services[proc.servicio] || 'Desconocido'} ); })()}
{/* 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.task_id}

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

{(() => { const estado = proc.status === 'pending' ? { text: 'En Espera', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' } : proc.status === 'processing' ? { text: 'Procesando', color: 'bg-blue-100 text-blue-800 border-blue-200' } : proc.status === 'completed' ? { text: 'Finalizado', color: 'bg-green-100 text-green-800 border-green-200' } : proc.status === 'failed' ? { text: 'Error', color: 'bg-red-100 text-red-800 border-red-200' } : { text: String(proc.status), color: 'bg-gray-100 text-gray-800 border-gray-200' }; return ( {estado.text} ); })()}
Pedimento: {proc.pedimento_app || '-'}
Fecha: {new Date(proc.timestamp).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true })}
Servicio: {(() => { const services = { '1': 'Estado de pedimento', '2': 'Listado de pedimentos', '3': 'Pedimento Completo', '4': 'Pedimento Partidas', '5': 'Pedimento Remesas', '6': 'Acuse', '7': 'EDocument', '8': 'Cove', '9': 'Acuse Cove' }; return ( {services[proc.servicio] || 'Desconocido'} ); })()}
)) )}
{/* Paginación compartida mejorada */} {count > 0 && (
{(() => { const totalPages = Math.max(1, Math.ceil(count / itemsPerPage)); const maxPagesToShow = 5; let startPage = Math.max(1, page - 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 => ( ))}
Página {page} de {totalPages} ); })()}
)} )}
); }