Files
frontend/src/pages/Procesos.jsx

1160 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-2xl p-6 max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900">Detalles de la Tarea</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-500"
>
<svg className="w-6 h-6" 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 className="space-y-6">
{/* Información básica de la tarea */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="p-4 bg-gray-50 rounded-xl">
<h4 className="text-sm font-medium text-gray-500">Task ID</h4>
<p className="mt-1 font-mono text-sm text-gray-900">{task.task_id}</p>
</div>
<div className="p-4 bg-gray-50 rounded-xl">
<h4 className="text-sm font-medium text-gray-500">Estado</h4>
<span className={`mt-2 inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold border ${getStatusColor(task.status)}`}>
{task.status}
</span>
</div>
<div className="p-4 bg-gray-50 rounded-xl">
<h4 className="text-sm font-medium text-gray-500">Fecha</h4>
<p className="mt-1 text-sm text-gray-900">
{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
})}
</p>
</div>
<div className="p-4 bg-gray-50 rounded-xl">
<h4 className="text-sm font-medium text-gray-500">Progreso</h4>
<p className="mt-1 text-sm text-gray-900">{task.progress || 0}%</p>
</div>
</div>
{/* Mensajes y Errores */}
<div className="p-4 bg-gray-50 rounded-xl">
<h4 className="text-sm font-medium text-gray-500">Mensaje de la tarea</h4>
{(() => {
// 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 (
<div className="mt-2 space-y-3">
<div className="p-3 rounded-lg bg-red-50">
<p className="text-sm font-medium text-red-700">{detail.message}</p>
</div>
{detail.errors && detail.errors.length > 0 && (
<div className="p-3 rounded-lg bg-red-50">
<ul className="pl-4 space-y-1 list-disc">
{detail.errors.map((error, idx) => (
<li key={idx} className="text-sm text-red-600">{error}</li>
))}
</ul>
</div>
)}
{detail.data && (
<div className="p-3 rounded-lg bg-orange-50">
<h5 className="mb-2 text-sm font-medium text-orange-700">Archivo de Error:</h5>
<p className="font-mono text-sm text-orange-600">{detail.data.error_file}</p>
</div>
)}
{detail.metadata && (
<div className="p-3 rounded-lg bg-blue-50">
<h5 className="mb-2 text-sm font-medium text-blue-700">Información Adicional:</h5>
<div className="grid grid-cols-2 gap-2">
{Object.entries(detail.metadata).map(([key, value]) => (
<div key={key} className="flex items-start gap-2">
<span className="text-sm font-medium text-blue-600">{key}:</span>
<span className="text-sm text-blue-700">{value}</span>
</div>
))}
</div>
</div>
)}
</div>
);
}
} catch (e) {
console.error('Error parsing message:', e);
}
}
// Si no se puede parsear, mostrar el mensaje original
return <p className="mt-1 text-sm text-gray-900">{task.message}</p>;
})()}
{/* Mostrar detalles de error si existe */}
{(task.status === 'FAILURE' || task.status === 'FAILED') && task.error && (
<div className="mt-4 space-y-4">
<div className="pt-4 border-t">
<h4 className="text-sm font-medium text-red-600">Detalles del Error</h4>
{(() => {
const errorDetail = parseError(task.error);
if (errorDetail.detail) {
return (
<div className="mt-3 space-y-4">
{/* Mensaje principal del error */}
{errorDetail.detail.message && (
<div className="p-3 rounded-lg bg-red-50">
<p className="text-sm text-red-700">{errorDetail.detail.message}</p>
</div>
)}
{/* Lista de errores específicos */}
{errorDetail.detail.errors && errorDetail.detail.errors.length > 0 && (
<div className="p-3 rounded-lg bg-red-50">
<h5 className="mb-2 text-sm font-medium text-red-700">Errores detectados:</h5>
<ul className="pl-4 space-y-1 list-disc">
{errorDetail.detail.errors.map((error, idx) => (
<li key={idx} className="text-sm text-red-600">{error}</li>
))}
</ul>
</div>
)}
{/* Datos adicionales del error */}
{errorDetail.detail.data && (
<div className="p-3 rounded-lg bg-orange-50">
<h5 className="mb-2 text-sm font-medium text-orange-700">Archivos relacionados:</h5>
<div className="grid grid-cols-1 gap-2">
{Object.entries(errorDetail.detail.data).map(([key, value]) => (
<div key={key} className="flex items-start gap-2">
<span className="text-sm font-medium text-orange-600">{key}:</span>
<span className="text-sm text-orange-700">{value}</span>
</div>
))}
</div>
</div>
)}
{/* Metadata */}
{errorDetail.detail.metadata && (
<div className="p-3 rounded-lg bg-blue-50">
<h5 className="mb-2 text-sm font-medium text-blue-700">Metadata:</h5>
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
{Object.entries(errorDetail.detail.metadata).map(([key, value]) => (
<div key={key} className="flex items-start gap-2">
<span className="text-sm font-medium text-blue-600">{key}:</span>
<span className="text-sm text-blue-700">{value}</span>
</div>
))}
</div>
</div>
)}
</div>
);
}
return (
<div className="p-3 mt-2 rounded-lg bg-red-50">
<p className="text-sm text-red-700">{task.error}</p>
</div>
);
})()}
</div>
</div>
)}
{/* Mensaje del resultado si existe */}
{task.result?.message && (
<>
<h4 className="mt-3 text-sm font-medium text-gray-500">Mensaje del resultado</h4>
<p className="mt-1 text-sm text-gray-900">{task.result.message}</p>
</>
)}
</div>
{/* Detalles del resultado */}
{task.result?.data && (
<div className="space-y-4">
<h4 className="pb-2 text-lg font-medium text-gray-700 border-b">Detalles del Resultado</h4>
{/* Información del documento si existe */}
{task.result.data.data?.documento && (
<div className="p-4 bg-blue-50 rounded-xl">
<h5 className="mb-3 text-sm font-medium text-blue-700">Información del Documento</h5>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<p className="text-xs font-medium text-blue-600">Número de Pedimento</p>
<p className="text-sm text-blue-900">{task.result.data.data.documento.pedimento_numero}</p>
</div>
<div>
<p className="text-xs font-medium text-blue-600">Tipo de Documento</p>
<p className="text-sm text-blue-900">{task.result.data.data.documento.document_type}</p>
</div>
<div>
<p className="text-xs font-medium text-blue-600">Tamaño</p>
<p className="text-sm text-blue-900">{task.result.data.data.documento.size.toLocaleString()} bytes</p>
</div>
<div>
<p className="text-xs font-medium text-blue-600">Extensión</p>
<p className="text-sm text-blue-900">{task.result.data.data.documento.extension}</p>
</div>
</div>
</div>
)}
{/* Información de la partida si existe */}
{task.result.data.data?.partida_update_response && (
<div className="p-4 bg-indigo-50 rounded-xl">
<h5 className="mb-3 text-sm font-medium text-indigo-700">Información de la Partida</h5>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<p className="text-xs font-medium text-indigo-600">Número de Partida</p>
<p className="text-sm text-indigo-900">{task.result.data.data.partida_update_response.numero_partida}</p>
</div>
<div>
<p className="text-xs font-medium text-indigo-600">Estado de Descarga</p>
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold ${task.result.data.data.partida_update_response.descargado ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
{task.result.data.data.partida_update_response.descargado ? 'Descargado' : 'Pendiente'}
</span>
</div>
</div>
</div>
)}
{/* Metadata si existe */}
{task.result.data?.metadata && (
<div className="p-4 bg-gray-50 rounded-xl">
<h5 className="mb-3 text-sm font-medium text-gray-700">Metadata</h5>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{Object.entries(task.result.data.metadata).map(([key, value]) => (
<div key={key}>
<p className="text-xs font-medium text-gray-500">{key}</p>
<p className="text-sm text-gray-900">{value}</p>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
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 (
<div className="min-h-screen p-4 bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 sm:p-6 lg:p-8">
{/* Modal de detalles del task */}
{selectedTask && (
<TaskDetailsModal
task={selectedTask}
onClose={() => setSelectedTask(null)}
/>
)}
{loadingTask && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="p-4 bg-white rounded-full">
<div className="w-8 h-8 border-b-2 border-blue-600 rounded-full animate-spin"></div>
</div>
</div>
)}
<div className="mx-auto max-w-7xl">
{/* Header mejorado y responsivo */}
<div className="relative flex items-center gap-4 p-6 mb-6 overflow-hidden shadow-2xl opacity-0 sm:mb-8 rounded-3xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 sm:p-8 sm:gap-6 animate-fadein-slideup"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex-shrink-0 p-3 rounded-full shadow-lg bg-white/20 backdrop-blur-sm sm:p-4 animate-bounce-slow">
<svg className="w-8 h-8 text-white sm:h-10 sm:w-10" 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">
<h1 className="flex flex-col gap-2 mb-1 text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl sm:flex-row sm:items-center">
<span>Procesos del Sistema</span>
{count > 0 && (
<span className="inline-block px-3 py-1 text-xs font-semibold text-white rounded-full shadow-lg bg-white/20 backdrop-blur-sm sm:text-sm animate-fade-in">
{count} procesos
</span>
)}
</h1>
<p className="text-sm font-medium leading-relaxed text-blue-100 sm:text-lg">Estado actual de los procesos de la agencia aduanal</p>
</div>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute pointer-events-none select-none -top-10 -right-10 opacity-20">
<div className="w-32 h-32 rounded-full bg-white/10 blur-xl"></div>
</div>
<div className="absolute pointer-events-none select-none -bottom-6 -left-6 opacity-15">
<div className="w-24 h-24 rounded-full bg-white/10 blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute w-2 h-2 rounded-full top-1/4 left-1/4 bg-white/30 animate-ping"></div>
<div className="absolute w-1 h-1 rounded-full top-3/4 right-1/3 bg-white/40 animate-pulse"></div>
<div className="absolute w-3 h-3 rounded-full top-1/2 right-1/4 bg-white/20 animate-bounce"></div>
</div>
{/* Animaciones CSS */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 3s infinite;
}
@keyframes fadein-slideup {
0% { opacity: 0; transform: translateY(40px); }
100% { opacity: 1; transform: translateY(0); }
}
.animate-fadein-slideup {
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.8s ease-out;
}
`}</style>
</div>
{/* Contenido principal */}
<div className="p-4 bg-white border border-gray-100 shadow-2xl opacity-0 rounded-3xl sm:p-6 lg:p-8 animate-fadein-slideup"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
<div className="flex flex-col justify-between gap-4 mb-6 sm:flex-row sm:items-center">
<h2 className="flex items-center gap-3 text-xl font-bold text-gray-900 sm:text-2xl">
<div className="p-2 shadow-lg bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl">
<svg className="w-5 h-5 text-white sm:w-6 sm:h-6" 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>
<div className="flex flex-col gap-3 sm:flex-row">
{count > 0 && (
<div className="px-4 py-2 border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl">
<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 responsivos mejorados */}
<div className="p-4 mb-6 border border-gray-100 bg-gradient-to-r from-gray-50 to-slate-50 rounded-2xl sm:p-6">
<h3 className="flex items-center gap-2 mb-4 text-lg font-semibold text-gray-800">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z" />
</svg>
Filtros de búsqueda
</h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
Pedimento
</label>
<input
type="text"
value={pedimentoPedimentoFilter}
onChange={e => {
setPedimentoPedimentoFilter(e.target.value);
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"
/>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
Estado
</label>
<select
value={statusFilter}
onChange={e => {
setStatusFilter(e.target.value);
setPage(1);
}}
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"
>
<option value="">Todos los estados</option>
<option value="submitted">Enviado</option>
<option value="processing">Procesando</option>
<option value="completed">Completado</option>
<option value="failed">Error</option>
</select>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
Servicio
</label>
<select
value={servicioFilter}
onChange={e => {
setServicioFilter(e.target.value);
setPage(1);
}}
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"
>
<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 className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<div className="w-2 h-2 bg-teal-500 rounded-full"></div>
Organización
</label>
<select
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"
disabled={loadingOrganizaciones}
value={organizacionFilter}
onChange={e => {
setOrganizacionFilter(e.target.value);
setPage(1);
}}
>
<option value="">Todas las organizaciones</option>
{loadingOrganizaciones ? (
<option value="" disabled>Cargando organizaciones...</option>
) : (
organizaciones.map((org) => (
<option key={org.id} value={org.id}>
{org.nombre}
</option>
))
)}
</select>
</div>
</div>
</div>
{/* BOTÓN PARA EJECUTAR PROCESAMIENTOS - AGREGAR AQUÍ */}
<div className="flex justify-end mb-6">
<div className="relative">
<button
type="button"
className="inline-flex items-center gap-2 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-semibold py-3 px-6 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5"
onClick={() => setShowProcesosDropdown(!showProcesosDropdown)}
disabled={ejecutandoProceso}
>
{ejecutandoProceso ? (
<>
<div className="w-4 h-4 border-b-2 border-white rounded-full animate-spin"></div>
Ejecutando...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Ejecutar Procesamiento
<svg
className={`w-4 h-4 transition-transform duration-200 ${showProcesosDropdown ? 'rotate-180' : ''}`}
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>
{/* Dropdown de opciones de procesamiento */}
{showProcesosDropdown && (
<div className="absolute right-0 z-50 w-64 mt-2 overflow-hidden bg-white border border-gray-200 shadow-2xl rounded-2xl animate-fade-in">
<div className="p-2">
{/* Encabezado del dropdown */}
<div className="px-3 py-2 mb-1 rounded-lg bg-gradient-to-r from-green-50 to-emerald-50">
<p className="text-sm font-semibold text-green-800">Selecciona un proceso</p>
<p className="text-xs text-green-600">Se ejecutará para tu organización</p>
</div>
{/* Opción "Todos" */}
<button
onClick={() => handleEjecutarProcesamiento({ todos: true })}
className="flex items-center w-full gap-3 px-4 py-3 mb-1 font-medium text-left text-gray-700 transition-colors duration-200 hover:bg-green-50 rounded-xl hover:text-green-700 group"
>
<div className="w-3 h-3 transition-transform duration-200 bg-green-500 rounded-full group-hover:scale-125"></div>
<div className="flex-1">
<span className="font-semibold">Todos</span>
<p className="text-xs text-gray-500 group-hover:text-green-600">Ejecutar todos los procesos</p>
</div>
<span className="text-xs text-gray-400 group-hover:text-green-500"></span>
</button>
<div className="my-2 border-t border-gray-100"></div>
{/* 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) => (
<button
key={proceso.id}
onClick={() => handleEjecutarProcesamiento({ procesamiento: proceso.id })}
className="flex items-center w-full gap-3 px-4 py-3 font-medium text-left text-gray-700 transition-colors duration-200 hover:bg-blue-50 rounded-xl hover:text-blue-700 group"
>
<div className="w-2 h-2 transition-transform duration-200 bg-blue-500 rounded-full group-hover:scale-125"></div>
<div className="flex-1">
<span>{proceso.label}</span>
<p className="text-xs text-gray-500 group-hover:text-blue-600">{proceso.desc}</p>
</div>
</button>
))}
</div>
</div>
)}
</div>
</div>
{/* Estados de carga y error mejorados */}
{loading ? (
<div className="flex flex-col items-center justify-center py-12">
<div className="relative">
<div className="w-16 h-16 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<div className="absolute inset-0 rounded-full bg-blue-500/10 blur-xl animate-pulse"></div>
</div>
<p className="mt-4 font-medium text-gray-600">Cargando procesos...</p>
</div>
) : error ? (
<div className="p-6 text-center border border-red-200 bg-red-50 rounded-2xl">
<div className="flex items-center justify-center w-12 h-12 p-3 mx-auto mb-4 bg-red-100 rounded-full">
<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="mb-2 text-lg font-semibold text-red-800">Error al cargar</h3>
<p className="text-red-600">{error}</p>
</div>
) : (
<>
{/* Vista de tabla para pantallas grandes */}
<div className="relative hidden pb-20 overflow-x-auto bg-white border border-gray-200 shadow-sm lg:block rounded-2xl"
style={{
overflowY: 'visible' // Permitir que los dropdowns se muestren fuera del contenedor
}}>
<table className="relative min-w-full divide-y divide-gray-300"
style={{ position: 'relative', zIndex: 1 }}>
<thead className="sticky top-0 z-10 bg-gray-50">
<tr>
<th className="px-4 py-4 text-xs font-bold tracking-wider text-center text-gray-600 uppercase transition-colors duration-200 cursor-pointer select-none hover:bg-gray-100"
onClick={() => {
setSortField('task_id');
setSortOrder(sortField === 'task_id' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center justify-center gap-1">
Task ID {sortField === 'task_id' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-xs font-bold tracking-wider text-left text-gray-600 uppercase transition-colors duration-200 cursor-pointer select-none hover:bg-gray-100"
onClick={() => {
setSortField('pedimento_app');
setSortOrder(sortField === 'pedimento_app' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Pedimento {sortField === 'pedimento_app' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-xs font-bold tracking-wider text-left text-gray-600 uppercase transition-colors duration-200 cursor-pointer select-none hover:bg-gray-100"
onClick={() => {
setSortField('status');
setSortOrder(sortField === 'status' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Estado {sortField === 'status' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-xs font-bold tracking-wider text-left text-gray-600 uppercase transition-colors duration-200 cursor-pointer select-none hover:bg-gray-100 rounded-tr-2xl"
onClick={() => {
setSortField('timestamp');
setSortOrder(sortField === 'timestamp' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Fecha de creación {sortField === 'timestamp' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-xs font-bold tracking-wider text-left text-gray-600 uppercase transition-colors duration-200 cursor-pointer select-none hover:bg-gray-100 rounded-tr-2xl"
onClick={() => {
setSortField('servicio');
setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
</tr>
</thead>
<tbody className="relative bg-white divide-y divide-gray-100" style={{ position: 'relative' }}>
{procesos.length === 0 ? (
<tr>
<td colSpan={5} className="py-12 text-center">
<div className="flex flex-col items-center">
<div className="flex items-center justify-center w-16 h-16 p-4 mx-auto mb-4 bg-gray-100 rounded-full">
<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="font-medium text-gray-500">No hay procesos disponibles</p>
<p className="mt-1 text-sm text-gray-400">Intenta ajustar los filtros de búsqueda</p>
</div>
</td>
</tr>
) : (
procesos.map((proc) => (
<tr key={proc.task_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">
<button
onClick={() => handleTaskClick(proc.task_id)}
className="px-2 py-1 text-sm font-semibold text-gray-800 transition-colors duration-200 bg-gray-100 rounded-lg cursor-pointer hover:bg-blue-100 hover:text-blue-800"
>
{proc.task_id}
</button>
</td>
<td className="px-4 py-4 text-sm font-medium text-gray-900 align-middle whitespace-nowrap">
<Link to={`/expedientes/pedimento/${proc.pedimento}`} className='hover:text-blue-500 hover:text-bold hover:text-underline'>
{proc.pedimento_app || '-'}
</Link>
</td>
<td className="px-4 py-4 align-middle whitespace-nowrap">
{(() => {
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 (
<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 text-sm text-gray-600 align-middle whitespace-nowrap">
{new Date(proc.timestamp).toLocaleString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
})}
</td>
<td className="px-4 py-4 align-middle whitespace-nowrap">
{(() => {
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 (
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold bg-indigo-50 text-indigo-700 border border-indigo-200">
{services[proc.servicio] || 'Desconocido'}
</span>
);
})()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="space-y-4 lg:hidden">
{procesos.length === 0 ? (
<div className="p-8 text-center bg-gray-50 rounded-2xl">
<div className="flex items-center justify-center w-16 h-16 p-4 mx-auto mb-4 bg-gray-100 rounded-full">
<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="font-medium text-gray-500">No hay procesos disponibles</p>
<p className="mt-1 text-sm text-gray-400">Intenta ajustar los filtros de búsqueda</p>
</div>
) : (
procesos.map((proc) => (
<div key={proc.task_id} className="p-4 transition-all duration-300 bg-white border border-gray-200 shadow-lg rounded-2xl hover:shadow-xl">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="flex-shrink-0 p-2 bg-blue-100 rounded-xl">
<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="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900">Proceso #{proc.task_id}</h3>
<p className="text-sm text-gray-500">{proc.organizacion_name || 'Sin organización'}</p>
</div>
</div>
{(() => {
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 (
<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="mb-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Pedimento:</span>
<span className="px-2 py-1 font-mono text-sm text-gray-900 bg-gray-100 rounded">
{proc.pedimento_app || '-'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Fecha:</span>
<span className="text-sm text-gray-600">
{new Date(proc.timestamp).toLocaleString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
})}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Servicio:</span>
{(() => {
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 (
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold bg-indigo-50 text-indigo-700 border border-indigo-200">
{services[proc.servicio] || 'Desconocido'}
</span>
);
})()}
</div>
</div>
))
)}
</div>
{/* Paginación compartida mejorada */}
{count > 0 && (
<div className="flex flex-col items-center justify-between gap-4 px-4 py-4 mt-6 border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50 sm:px-6 rounded-2xl sm:flex-row">
{(() => {
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 (
<>
<div className="flex items-center gap-3">
<label htmlFor="itemsPerPage" className="text-sm font-medium text-gray-600">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
className="px-3 py-2 text-sm bg-white border border-gray-300 shadow-sm rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{[5, 8, 12, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={e => { e.preventDefault(); setPage(1); }}
disabled={page === 1}
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
type="button"
onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }}
disabled={page === 1}
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>
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => { e.preventDefault(); setPage(num); }}
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}
</button>
))}
<button
type="button"
onClick={e => { e.preventDefault(); setPage(p => p + 1); }}
disabled={page >= totalPages}
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
type="button"
onClick={e => { e.preventDefault(); setPage(totalPages); }}
disabled={page >= totalPages}
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>
</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>
);
}