diff --git a/src/api/procesos.ts b/src/api/procesos.ts index 943b64e..1170831 100644 --- a/src/api/procesos.ts +++ b/src/api/procesos.ts @@ -1,31 +1,29 @@ import { fetchWithAuth } from '../fetchWithAuth'; // Tipos para la respuesta y registros -export interface ProcesamientoPedimento { - id: number; - created_at: string; - updated_at: string; +export interface Task { + task_id: string; + timestamp: string; + message: string; + status: string; + pedimento: string; organizacion: string; - organizacion_name: string; - estado: number; - tipo_procesamiento: number; - pedimento: string; servicio: number; } -export interface ProcesamientoPedimentosResponse { +export interface TasksResponse { count: number; next: string | null; previous: string | null; - results: ProcesamientoPedimento[]; + results: Task[]; } -// API para customs/procesamientopedimentos/ -export async function fetchProcesamientoPedimentos( +// API para tasks/tasks/ +export async function fetchTasks( page: number = 1, pageSize: number = 20, filters: Record = {} -): Promise { +): Promise { try { const API_URL = (import.meta as any).env.VITE_EFC_API_URL; @@ -41,15 +39,15 @@ export async function fetchProcesamientoPedimentos( } }); - const res = await fetchWithAuth(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`); + const res = await fetchWithAuth(`${API_URL}/tasks/tasks/?${params.toString()}`); if (!res.ok) { - throw new Error('Error al obtener procesamiento de pedimentos'); + throw new Error('Error al obtener tareas'); } return await res.json(); } catch (error) { - console.error('Error in fetchProcesamientoPedimentos:', error); + console.error('Error in fetchTasks:', error); throw error; } } diff --git a/src/api/taskStatus.js b/src/api/taskStatus.js new file mode 100644 index 0000000..8620d12 --- /dev/null +++ b/src/api/taskStatus.js @@ -0,0 +1,36 @@ +// Helper functions for task status display + +export const getTaskStatusLabel = (status) => { + const statuses = { + 'submitted': 'Enviado', + 'pending': 'Pendiente', + 'processing': 'Procesando', + 'completed': 'Completado', + 'failed': 'Error', + 'cancelled': 'Cancelado' + }; + return statuses[status] || `Estado ${status}`; +}; + +export const getTaskStatusColor = (status) => { + const colors = { + 'submitted': 'bg-blue-100 text-blue-800', + 'pending': 'bg-yellow-100 text-yellow-800', + 'processing': 'bg-indigo-100 text-indigo-800', + 'completed': 'bg-green-100 text-green-800', + 'failed': 'bg-red-100 text-red-800', + 'cancelled': 'bg-gray-100 text-gray-800' + }; + return colors[status] || 'bg-gray-100 text-gray-800'; +}; + +// Ayuda a determinar si el estado permite ciertas acciones +export const isTaskActionable = (status) => { + const nonActionableStatuses = ['processing', 'completed', 'cancelled']; + return !nonActionableStatuses.includes(status); +}; + +export const isTaskFinal = (status) => { + const finalStatuses = ['completed', 'failed', 'cancelled']; + return finalStatuses.includes(status); +}; \ No newline at end of file diff --git a/src/pages/Auditor.jsx b/src/pages/Auditor.jsx index 832aac0..73517eb 100644 --- a/src/pages/Auditor.jsx +++ b/src/pages/Auditor.jsx @@ -1,4 +1,6 @@ + import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; import { getWithAuth, postWithAuth } from '../fetchWithAuth'; const API_URL = import.meta.env.VITE_EFC_API_URL; @@ -15,13 +17,92 @@ function Auditor() { const [auditandoRemesas, setAuditandoRemesas] = useState(false); const [procesandoPedimento, setProcesandoPedimento] = useState(null); const [procesandoRemesa, setProcesandoRemesa] = useState(null); + const [procesandoAcuse, setProcesandoAcuse] = useState(null); + const [procesandoCove, setProcesandoCove] = useState(null); + const [auditandoAcusesCove, setAuditandoAcusesCove] = useState(false); + const [pedimentoFilter, setPedimentoFilter] = useState(''); + const [auditandoEDocuments, setAuditandoEDocuments] = useState(false); + const [auditandoAcuses, setAuditandoAcuses] = useState(false); + + // Handler para auditar acuses (general) + const handleAuditarAcuses = async () => { + if (auditandoAcuses) return; + try { + setAuditandoAcuses(true); + const organizacionId = pedimentos[0]?.organizacion; + if (!organizacionId) { + throw new Error('No hay organización disponible para auditar'); + } + const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-acuse/`, { + organizacion_id: organizacionId + }); + if (!response.ok) { + throw new Error('Error al iniciar la auditoría de acuses'); + } + alert('La auditoría de acuses se ha iniciado correctamente'); + } catch (error) { + console.error('Error:', error); + alert(error.message); + } finally { + setAuditandoAcuses(false); + } + }; + + // Handler para auditar EDocuments (general) + const handleAuditarEDocuments = async () => { + if (auditandoEDocuments) return; + try { + setAuditandoEDocuments(true); + const organizacionId = pedimentos[0]?.organizacion; + if (!organizacionId) { + throw new Error('No hay organización disponible para auditar'); + } + const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-edocuments/`, { + organizacion_id: organizacionId + }); + if (!response.ok) { + throw new Error('Error al iniciar la auditoría de EDocuments'); + } + alert('La auditoría de EDocuments se ha iniciado correctamente'); + } catch (error) { + console.error('Error:', error); + alert(error.message); + } finally { + setAuditandoEDocuments(false); + } + }; + + // Handler para auditar acuses cove (general) + const handleAuditarAcusesCove = async () => { + if (auditandoAcusesCove) return; + try { + setAuditandoAcusesCove(true); + // Obtener el organizacion_id del primer pedimento + const organizacionId = pedimentos[0]?.organizacion; + if (!organizacionId) { + throw new Error('No hay organización disponible para auditar'); + } + const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-acuse-cove/`, { + organizacion_id: organizacionId + }); + if (!response.ok) { + throw new Error('Error al iniciar la auditoría de acuses cove'); + } + alert('La auditoría de acuses cove se ha iniciado correctamente'); + } catch (error) { + console.error('Error:', error); + alert(error.message); + } finally { + setAuditandoAcusesCove(false); + } + }; const handleAuditarRemesaPedimento = async (pedimentoId) => { if (procesandoRemesa) return; - + try { setProcesandoRemesa(pedimentoId); - + const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-procesamiento-remesa/pedimento/`, { pedimento_id: pedimentoId }); @@ -43,12 +124,12 @@ function Auditor() { const handleAuditarRemesas = async () => { if (auditandoRemesas) return; - + try { setAuditandoRemesas(true); // Obtener el organizacion_id del primer pedimento const organizacionId = pedimentos[0]?.organizacion; - + if (!organizacionId) { throw new Error('No hay organización disponible para auditar'); } @@ -74,10 +155,10 @@ function Auditor() { const handleAuditarPartidasPedimento = async (pedimentoId) => { if (procesandoPedimento) return; - + try { setProcesandoPedimento(pedimentoId); - + const response = await postWithAuth(`${API_URL}/customs/auditor/crear-partidas/pedimento/`, { pedimento_id: pedimentoId }); @@ -99,12 +180,12 @@ function Auditor() { const handleAuditarPartidas = async () => { if (auditandoPartidas) return; - + try { setAuditandoPartidas(true); // Obtener el organizacion_id del primer pedimento const organizacionId = pedimentos[0]?.organizacion; - + if (!organizacionId) { throw new Error('No hay organización disponible para auditar'); } @@ -130,12 +211,12 @@ function Auditor() { const handleAuditarTodos = async () => { if (auditando) return; - + try { setAuditando(true); // Obtener el organizacion_id del primer pedimento const organizacionId = pedimentos[0]?.organizacion; - + if (!organizacionId) { throw new Error('No hay organización disponible para auditar'); } @@ -159,11 +240,54 @@ function Auditor() { } }; + const handleAuditarAcusePedimento = async (pedimentoId) => { + if (procesandoAcuse) return; + try { + setProcesandoAcuse(pedimentoId); + const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-acuse/pedimento/`, { + pedimento_id: pedimentoId + }); + if (!response.ok) { + throw new Error('Error al auditar acuse del pedimento'); + } + alert('El acuse del pedimento se está auditando'); + } catch (error) { + console.error('Error:', error); + alert(error.message); + } finally { + setProcesandoAcuse(null); + } + }; + + const handleAuditarCovePedimento = async (pedimentoId) => { + if (procesandoCove) return; + try { + setProcesandoCove(pedimentoId); + const response = await postWithAuth(`${API_URL}/customs/auditor/auditar-cove/pedimento/`, { + pedimento_id: pedimentoId + }); + if (!response.ok) { + throw new Error('Error al auditar COVE del pedimento'); + } + alert('El COVE del pedimento se está auditando'); + } catch (error) { + console.error('Error:', error); + alert(error.message); + } finally { + setProcesandoCove(null); + } + }; + useEffect(() => { const fetchPedimentos = async () => { setLoading(true); try { - const response = await getWithAuth(`${API_URL}/customs/pedimentos/?page=${page}&page_size=${itemsPerPage}`); + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: itemsPerPage.toString(), + ...(pedimentoFilter && { pedimento_app: pedimentoFilter }) + }); + const response = await getWithAuth(`${API_URL}/customs/pedimentos/?${queryParams}`); if (!response.ok) throw new Error('Error al cargar los pedimentos'); const data = await response.json(); setPedimentos(data.results); @@ -174,14 +298,20 @@ function Auditor() { setLoading(false); } }; - fetchPedimentos(); - }, [page, itemsPerPage]); + + // Aplicar debounce al fetchPedimentos + const timeoutId = setTimeout(() => { + fetchPedimentos(); + }, 300); + + return () => clearTimeout(timeoutId); + }, [page, itemsPerPage, pedimentoFilter]); return (
{/* Header mejorado y responsivo */} -
@@ -223,9 +353,9 @@ function Auditor() {
{/* Contenido principal */} -
- + {loading ? (
@@ -247,6 +377,7 @@ function Auditor() { ) : (
+

@@ -254,8 +385,91 @@ function Auditor() { Servicios de Auditoría

-
+ + + + + +
+ {/* Sección de Filtros */} +
+
+ {/* Contenedor para los filtros - esperando instrucciones */} +
+

+ Filtros de Búsqueda +

+
+
+ +
+ setPedimentoFilter(e.target.value)} + className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-3 pr-10 py-2 sm:text-sm border-gray-300 rounded-md" + placeholder="Buscar por pedimento..." + /> +
+ + + +
+
+
+
+
+ {/* Tabla de pedimentos */}
@@ -384,23 +631,23 @@ function Auditor() { {pedimentos.map((pedimento) => ( {/* PC - Pedimento Completo */} {/* RM - Remesas */} {/* PT - Partidas */} {/* AC - Acuse */} {/* COVE */} {/* AC_COVE */} @@ -470,7 +745,7 @@ function Auditor() { diff --git a/src/pages/PedimentoDetail.jsx b/src/pages/PedimentoDetail.jsx index 3f58494..4894b5d 100644 --- a/src/pages/PedimentoDetail.jsx +++ b/src/pages/PedimentoDetail.jsx @@ -17,9 +17,10 @@ import 'highlight.js/styles/github.css'; hljs.registerLanguage('xml', xml); import { fetchPedimentoDocuments } from '../api/pedimentoDocuments'; import { fetchWithAuth, postWithAuth, putWithAuth } from '../fetchWithAuth'; -import { fetchProcesamientoPedimentos } from '../api/procesos.ts'; +import { fetchTasks } from '../api/procesos.ts'; import { fetchPedimentoCoves, downloadCove, downloadAcuseCove } from '../api/coves'; import { fetchPedimentoEdocuments, downloadEdocument, downloadAcuseEdocument } from '../api/edocuments'; +import { getTaskStatusLabel, getTaskStatusColor, isTaskActionable, isTaskFinal } from '../api/taskStatus'; import { useParams, Link } from 'react-router-dom'; import { useNotification } from '../context/NotificationContext'; @@ -370,11 +371,21 @@ export default function PedimentoDetail() { const [procesosPage, setProcesosPage] = useState(1); const [procesosPageSize, setProcesosPageSize] = useState(10); const [procesosFilters, setProcesosFilters] = useState({}); + const [sortField, setSortField] = useState(''); + const [sortOrder, setSortOrder] = useState('asc'); + const [selectedProcesos, setSelectedProcesos] = useState([]); + const [isSelectAll, setIsSelectAll] = useState(false); // Estados para las acciones de procesos const [executingId, setExecutingId] = useState(null); const [changingStateId, setChangingStateId] = useState(null); const [creatingService, setCreatingService] = useState(null); + + // Ref para rastrear valores previos de filtros + const prevFiltersRef = useRef({ + sortField: '', + sortOrder: 'asc' + }); // Estados para modal de preview const [previewOpen, setPreviewOpen] = useState(false); @@ -448,7 +459,7 @@ export default function PedimentoDetail() { setSelected(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); }; - const handleSelectAll = () => { + const handleSelectAllDocs = () => { if (allSelected) setSelected([]); else setSelected(allDocIds); }; @@ -824,10 +835,22 @@ export default function PedimentoDetail() { }, [edocsFilters]); // Funciones para acciones de Procesos + // Map legacy estado values to new status values + const mapEstadoToStatus = (estado) => { + const mapping = { + 1: 'pending', // Pendiente + 2: 'processing', // En Proceso + 3: 'completed', // Completado + 4: 'failed', // Error + 5: 'cancelled' // Cancelado + }; + return mapping[estado] || 'pending'; + }; + const updateProcesoEstado = (procId, nuevoEstado) => { setProcesos(procesos => procesos.map(proc => - proc.id === procId ? { ...proc, estado: nuevoEstado } : proc + proc.id === procId ? { ...proc, status: mapEstadoToStatus(nuevoEstado) } : proc ) ); }; @@ -850,7 +873,7 @@ export default function PedimentoDetail() { organizacion: pedimentoData?.organizacion || 1 // Usar la organización del pedimento o default }; - const res = await postWithAuth(`${API_URL}/customs/procesamientopedimentos/`, body); + const res = await postWithAuth(`${API_URL}/tasks/tasks/`, body); if (!res.ok) { throw new Error(`Error al crear el servicio: ${nombreServicio}`); @@ -871,7 +894,7 @@ export default function PedimentoDetail() { ...procesosFilters, pedimento: id }; - fetchProcesamientoPedimentos(procesosPage, procesosPageSize, filters) + fetchTasks(procesosPage, procesosPageSize, filters) .then((data) => { setProcesos(data.results); setProcesosCount(data.count); @@ -893,33 +916,33 @@ export default function PedimentoDetail() { setChangingStateId(proc.id); // Cambiar estado visual a "Procesando" inmediatamente - updateProcesoEstado(proc.id, 2); // 2 = Procesando + updateProcesoEstado(proc.id, 'processing'); try { const body = { - estado: 1, // Cambiar a En Espera + status: 'pending', // Cambiar a Pendiente tipo_procesamiento: 2, pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, servicio: proc.servicio, organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId }; - const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); + const res = await putWithAuth(`${API_URL}/tasks/tasks/${proc.task_id}/`, body); if (!res.ok) { - // Si falla, revertir a estado Error - updateProcesoEstado(proc.id, 4); // 4 = Error + // Si falla, cambiar a estado Error + updateProcesoEstado(proc.id, 'failed'); // Failed throw new Error('Error al cambiar el estado del proceso'); } // Cambiar estado visual a "En Espera" si fue exitoso - updateProcesoEstado(proc.id, 1); // 1 = En Espera + updateProcesoEstado(proc.id, 'pending'); showMessage('Estado cambiado a "En Espera" correctamente', 'success'); } catch (err) { console.error('Error cambiando estado:', err); - updateProcesoEstado(proc.id, 4); // 4 = Error + updateProcesoEstado(proc.id, 'failed'); showMessage('Error al cambiar el estado del proceso: ' + err.message, 'error'); } finally { setChangingStateId(null); @@ -930,7 +953,7 @@ export default function PedimentoDetail() { setExecutingId(proc.id); // Cambiar estado visual a "Procesando" inmediatamente - updateProcesoEstado(proc.id, 2); // 2 = Procesando + updateProcesoEstado(proc.id, 'processing'); let endpoint = ''; // Determinar endpoint según el tipo de servicio @@ -958,7 +981,7 @@ export default function PedimentoDetail() { break; default: // Revertir estado si el servicio no es soportado - updateProcesoEstado(proc.id, proc.estado); // Revertir al estado original + updateProcesoEstado(proc.id, proc.status); // Revertir al estado original showMessage('Este servicio no es compatible para ejecución directa.', 'error'); setExecutingId(null); return; @@ -974,17 +997,17 @@ export default function PedimentoDetail() { if (!res.ok) { // Si falla, cambiar estado a Error - updateProcesoEstado(proc.id, 4); // 4 = Error + updateProcesoEstado(proc.id, 'failed'); throw new Error('Error al ejecutar el servicio'); } // Si es exitoso, cambiar estado a Finalizado - updateProcesoEstado(proc.id, 3); // 3 = Finalizado + updateProcesoEstado(proc.id, 'completed'); showMessage('El servicio se ha ejecutado correctamente', 'success'); } catch (err) { // Cambiar estado a Error en caso de excepción - updateProcesoEstado(proc.id, 4); // 4 = Error + updateProcesoEstado(proc.id, 'failed'); if (err.message === 'SESSION_EXPIRED') { showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error'); @@ -1257,6 +1280,57 @@ export default function PedimentoDetail() { } } }; + + // Funciones de manejo de procesos + // Removed duplicate definitions + + // Handle select all process checkboxes + const handleSelectAllProcesos = (e) => { + if (e.target.checked) { + const availableProcesos = procesos + .filter(proc => proc.status !== 'running' && proc.status !== 'completed') + .map(proc => proc.task_id); + setSelectedProcesos(availableProcesos); + setIsSelectAll(true); + } else { + setSelectedProcesos([]); + setIsSelectAll(false); + } + }; + + // Handle select individual process checkbox + const handleSelectProceso = (id, checked) => { + if (checked) { + setSelectedProcesos(prev => [...prev, id]); + } else { + setSelectedProcesos(prev => prev.filter(i => i !== id)); + } + // Update isSelectAll + const availableProcesos = procesos + .filter(proc => proc.status !== 'running' && proc.status !== 'completed') + .map(proc => proc.task_id); + setIsSelectAll(availableProcesos.length === selectedProcesos.length + (checked ? 1 : -1)); + }; + + const handlePasarPaginaAEspera = async () => { + const failedProcesses = procesos.filter(proc => proc.status === 'failed'); + if (failedProcesses.length === 0) return; + + for (const proceso of failedProcesses) { + await handlePasarAEspera(proceso); + } + }; + + const handlePasarSeleccionadosAEspera = async () => { + const failedSelectedProcesses = procesos.filter( + proc => selectedProcesos.includes(proc.task_id) && proc.status === 'failed' + ); + if (failedSelectedProcesses.length === 0) return; + + for (const proceso of failedSelectedProcesses) { + await handlePasarAEspera(proceso); + } + }; // Función para verificar servicios creados const handleVerificarServiciosCreados = async () => { @@ -1347,17 +1421,48 @@ export default function PedimentoDetail() { // Fetch Procesos cuando sea necesario useEffect(() => { if (!id || activeTab !== 'procesos') return; + + // Detectar si algún filtro cambió + const currentFilters = { + 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 && procesosPage !== 1) { + setProcesosPage(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 }; setProcesosLoading(true); setProcesosError(''); - // Crear filtros incluyendo el pedimento + // Construir filtros const filters = { ...procesosFilters, pedimento: id // Filtrar por el pedimento actual }; - fetchProcesamientoPedimentos(procesosPage, procesosPageSize, filters) + if (sortField) { + const fieldMapping = { + 'id': 'task_id', + 'estado': 'status', + 'pedimento': 'pedimento_app' + }; + const mappedField = fieldMapping[sortField] || sortField; + filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + mappedField; + } + + fetchTasks(procesosPage, procesosPageSize, filters) .then((data) => { setProcesos(data.results); setProcesosCount(data.count); @@ -1372,7 +1477,7 @@ export default function PedimentoDetail() { } setProcesosLoading(false); }); - }, [id, activeTab, procesosPage, procesosPageSize, showMessage]); + }, [id, activeTab, procesosPage, procesosPageSize, sortField, sortOrder, procesosFilters, showMessage]); // Resetear página de Procesos cuando cambie el pedimento useEffect(() => { @@ -3610,7 +3715,18 @@ export default function PedimentoDetail() { {procesos.map((proceso, index) => ( - + + diff --git a/src/pages/Procesos.jsx b/src/pages/Procesos.jsx index a1b71bf..5c19ea1 100644 --- a/src/pages/Procesos.jsx +++ b/src/pages/Procesos.jsx @@ -1,15 +1,301 @@ import React, { useEffect, useState, useRef } from 'react'; import { Link } from 'react-router-dom'; -import { fetchProcesamientoPedimentos } from '../api/procesos.ts'; -import { postWithAuth, putWithAuth } from '../fetchWithAuth'; -const API_URL = import.meta.env.VITE_EFC_API_URL; -const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL; +import { fetchTasks } from '../api/procesos.ts'; +import { fetchWithAuth } from '../fetchWithAuth'; -// Estado para loading de ejecución de servicio -// y función para ejecutar el servicio según el tipo de proceso +// 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() { @@ -19,692 +305,71 @@ export default function Procesos() { const [page, setPage] = useState(1); const [count, setCount] = useState(0); const [itemsPerPage, setItemsPerPage] = useState(12); - // Filtros - const [pedimentoPedimentoFilter, setPedimentoPedimentoFilter] = useState(''); - const [estadoFilter, setEstadoFilter] = useState(''); - const [servicioFilter, setServicioFilter] = useState(''); + 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(''); + // Sorting const [sortField, setSortField] = useState(''); const [sortOrder, setSortOrder] = useState('asc'); // 'asc' | 'desc' - // Estado para loading de ejecución de servicio - const [executingId, setExecutingId] = useState(null); - // Estado para loading de cambio de estado - const [changingStateId, setChangingStateId] = useState(null); - - // Estado para el sistema de toast - const [toasts, setToasts] = useState([]); - - // Estado para selección masiva - const [selectedProcesos, setSelectedProcesos] = useState([]); - const [isSelectAll, setIsSelectAll] = useState(false); - // Ref para rastrear valores previos de filtros y detectar cambios const prevFiltersRef = useRef({ pedimentoPedimentoFilter: '', - estadoFilter: '', + statusFilter: '', servicioFilter: '', sortField: '', sortOrder: 'asc' }); - - // Función para mostrar toast - const showToast = (type, title, message, details = '', persistent = false, progress = null) => { - const id = Date.now(); - const newToast = { - id, - type, - title, - message, - details, - isVisible: true, - persistent, - progress - }; - - setToasts(prev => [...prev, newToast]); - - // Auto remover el toast después de 5 segundos (8 segundos para info) solo si no es persistente - if (!persistent) { - const timeout = type === 'info' ? 8000 : 5000; - setTimeout(() => { - removeToast(id); - }, timeout); - } - - return id; // Retornar el ID para poder actualizar el toast - }; - - // Función para actualizar toast existente (especialmente útil para progreso) - const updateToast = (id, updates) => { - setToasts(prev => prev.map(toast => - toast.id === id ? { ...toast, ...updates } : toast - )); - }; - - // Función para remover toast - const removeToast = (id) => { - setToasts(prev => prev.filter(toast => toast.id !== id)); - }; - - // Función para actualizar el estado de un proceso específico - const updateProcesoEstado = (procId, nuevoEstado) => { - setProcesos(prev => prev.map(proc => - proc.id === procId ? { ...proc, estado: nuevoEstado } : proc - )); - }; - - // Funciones para manejo de selección - const handleSelectProceso = (procesoId, isSelected) => { - if (isSelected) { - setSelectedProcesos(prev => [...prev, procesoId]); - } else { - setSelectedProcesos(prev => prev.filter(id => id !== procesoId)); - } - }; - - const handleSelectAll = () => { - if (isSelectAll) { - setSelectedProcesos([]); - setIsSelectAll(false); - } else { - // Solo seleccionar procesos que se pueden ejecutar (En Espera o Error) - const procesosEjecutables = procesos.filter(proc => proc.estado === 1 || proc.estado === 4); - setSelectedProcesos(procesosEjecutables.map(proc => proc.id)); - setIsSelectAll(true); - } - }; - - // Función para procesar página entera - const handleProcesarPaginaEntera = async () => { - const procesosEjecutables = procesos.filter(proc => proc.estado === 1 || proc.estado === 4); - - if (procesosEjecutables.length === 0) { - showToast('warning', 'Sin procesos ejecutables', 'No hay procesos en estado "En Espera" o "Error" para procesar.'); - return; - } - - // Calcular tiempo estimado (aproximadamente 2-3 segundos por proceso) - const tiempoEstimadoSegundos = procesosEjecutables.length * 2.5; - const minutos = Math.floor(tiempoEstimadoSegundos / 60); - const segundos = Math.round(tiempoEstimadoSegundos % 60); - const tiempoTexto = minutos > 0 ? `${minutos}m ${segundos}s` : `${segundos}s`; - - // Crear toast persistente con progreso - const progressToastId = showToast('info', 'Procesamiento Masivo en Progreso', - `Procesando ${procesosEjecutables.length} procesos...\n⏱️ Tiempo estimado: ${tiempoTexto}`, - '', - true, // persistente - { current: 0, total: procesosEjecutables.length, percentage: 0 } - ); - - const inicioTiempo = Date.now(); - let exitosos = 0; - let errores = 0; - - for (let i = 0; i < procesosEjecutables.length; i++) { - const proc = procesosEjecutables[i]; - - // Actualizar progreso en tiempo real - const tiempoTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const progresoPercentage = Math.round(((i + 1) / procesosEjecutables.length) * 100); - const tiempoRestanteEstimado = i > 0 ? Math.round((tiempoTranscurrido / i) * (procesosEjecutables.length - i)) : tiempoEstimadoSegundos; - - updateToast(progressToastId, { - message: `Procesando ${procesosEjecutables.length} procesos...\n⏱️ Transcurrido: ${tiempoTranscurrido}s | Restante: ~${tiempoRestanteEstimado}s\n📊 Proceso actual: ${proc.pedimento?.numero || proc.pedimento} (${i + 1}/${procesosEjecutables.length})`, - progress: { - current: i + 1, - total: procesosEjecutables.length, - percentage: progresoPercentage, - exitosos, - errores - } - }); - - try { - // Cambiar estado visual a "Procesando" - updateProcesoEstado(proc.id, 2); - - // Determinar endpoint según el tipo de servicio - let endpoint = ''; - switch (proc.servicio) { - case 3: endpoint = '/services/pedimento_completo'; break; - case 4: endpoint = '/services/partidas'; break; - case 5: endpoint = '/services/remesas'; break; - case 6: endpoint = '/services/acuse'; break; - case 7: endpoint = '/services/edocument'; break; - case 8: endpoint = '/services/coves'; break; - case 9: endpoint = '/services/acuseCove'; break; - default: - updateProcesoEstado(proc.id, 4); - errores++; - continue; - } - - const body = { - pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, - organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId, - }; - - const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); - - if (!res.ok) { - updateProcesoEstado(proc.id, 4); - errores++; - } else { - updateProcesoEstado(proc.id, 3); - exitosos++; - } - - // Pequeña pausa entre requests para no sobrecargar - await new Promise(resolve => setTimeout(resolve, 500)); - } catch (err) { - updateProcesoEstado(proc.id, 4); - errores++; - } - } - - const tiempoTotalTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const minutosTotales = Math.floor(tiempoTotalTranscurrido / 60); - const segundosTotales = tiempoTotalTranscurrido % 60; - const tiempoTotalTexto = minutosTotales > 0 ? `${minutosTotales}m ${segundosTotales}s` : `${segundosTotales}s`; - - // Actualizar toast final - updateToast(progressToastId, { - type: 'success', - title: '✅ Procesamiento Completado', - message: `Resultados finales:\n✓ ${exitosos} exitosos | ✗ ${errores} errores\n⏱️ Tiempo total: ${tiempoTotalTexto}`, - progress: { - current: procesosEjecutables.length, - total: procesosEjecutables.length, - percentage: 100, - exitosos, - errores, - completed: true - } - }); - }; - - // Función para procesar seleccionados - const handleProcesarSeleccionados = async () => { - if (selectedProcesos.length === 0) { - showToast('warning', 'Sin selección', 'No hay procesos seleccionados para procesar.'); - return; - } - - const procesosAEjecutar = procesos.filter(proc => - selectedProcesos.includes(proc.id) && (proc.estado === 1 || proc.estado === 4) - ); - - if (procesosAEjecutar.length === 0) { - showToast('warning', 'Sin procesos ejecutables', 'Los procesos seleccionados no están en estado "En Espera" o "Error".'); - return; - } - - // Calcular tiempo estimado (aproximadamente 2-3 segundos por proceso) - const tiempoEstimadoSegundos = procesosAEjecutar.length * 2.5; - const minutos = Math.floor(tiempoEstimadoSegundos / 60); - const segundos = Math.round(tiempoEstimadoSegundos % 60); - const tiempoTexto = minutos > 0 ? `${minutos}m ${segundos}s` : `${segundos}s`; - - // Crear toast persistente con progreso - const progressToastId = showToast('info', 'Procesamiento de Seleccionados en Progreso', - `Procesando ${procesosAEjecutar.length} procesos seleccionados...\n⏱️ Tiempo estimado: ${tiempoTexto}`, - '', - true, // persistente - { current: 0, total: procesosAEjecutar.length, percentage: 0 } - ); - - const inicioTiempo = Date.now(); - let exitosos = 0; - let errores = 0; - - for (let i = 0; i < procesosAEjecutar.length; i++) { - const proc = procesosAEjecutar[i]; - - // Actualizar progreso en tiempo real - const tiempoTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const progresoPercentage = Math.round(((i + 1) / procesosAEjecutar.length) * 100); - const tiempoRestanteEstimado = i > 0 ? Math.round((tiempoTranscurrido / i) * (procesosAEjecutar.length - i)) : tiempoEstimadoSegundos; - - updateToast(progressToastId, { - message: `Procesando ${procesosAEjecutar.length} procesos seleccionados...\n⏱️ Transcurrido: ${tiempoTranscurrido}s | Restante: ~${tiempoRestanteEstimado}s\n📊 Proceso actual: ${proc.pedimento?.numero || proc.pedimento} (${i + 1}/${procesosAEjecutar.length})`, - progress: { - current: i + 1, - total: procesosAEjecutar.length, - percentage: progresoPercentage, - exitosos, - errores - } - }); - - try { - // Cambiar estado visual a "Procesando" - updateProcesoEstado(proc.id, 2); - - // Determinar endpoint según el tipo de servicio - let endpoint = ''; - switch (proc.servicio) { - case 3: endpoint = '/services/pedimento_completo'; break; - case 4: endpoint = '/services/partidas'; break; - case 5: endpoint = '/services/remesas'; break; - case 6: endpoint = '/services/acuse'; break; - case 7: endpoint = '/services/edocument'; break; - case 8: endpoint = '/services/coves'; break; - case 9: endpoint = '/services/acuseCove'; break; - default: - updateProcesoEstado(proc.id, 4); - errores++; - continue; - } - - const body = { - pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, - organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId, - }; - - const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); - - if (!res.ok) { - updateProcesoEstado(proc.id, 4); - errores++; - } else { - updateProcesoEstado(proc.id, 3); - exitosos++; - } - - // Pequeña pausa entre requests para no sobrecargar - await new Promise(resolve => setTimeout(resolve, 500)); - } catch (err) { - updateProcesoEstado(proc.id, 4); - errores++; - } - } - - // Limpiar selección después del procesamiento - setSelectedProcesos([]); - setIsSelectAll(false); - - const tiempoTotalTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const minutosTotales = Math.floor(tiempoTotalTranscurrido / 60); - const segundosTotales = tiempoTotalTranscurrido % 60; - const tiempoTotalTexto = minutosTotales > 0 ? `${minutosTotales}m ${segundosTotales}s` : `${segundosTotales}s`; - - // Actualizar toast final - updateToast(progressToastId, { - type: 'success', - title: '✅ Procesamiento de Seleccionados Completado', - message: `Resultados finales:\n✓ ${exitosos} exitosos | ✗ ${errores} errores\n⏱️ Tiempo total: ${tiempoTotalTexto}`, - progress: { - current: procesosAEjecutar.length, - total: procesosAEjecutar.length, - percentage: 100, - exitosos, - errores, - completed: true - } - }); - }; - - // Función para pasar página entera a En Espera - const handlePasarPaginaAEspera = async () => { - const procesosEnError = procesos.filter(proc => proc.estado === 4); - - if (procesosEnError.length === 0) { - showToast('warning', 'Sin procesos en error', 'No hay procesos en estado "Error" para pasar a "En Espera".'); - return; - } - - // Calcular tiempo estimado (aproximadamente 1 segundo por proceso) - const tiempoEstimadoSegundos = procesosEnError.length * 1; - const minutos = Math.floor(tiempoEstimadoSegundos / 60); - const segundos = Math.round(tiempoEstimadoSegundos % 60); - const tiempoTexto = minutos > 0 ? `${minutos}m ${segundos}s` : `${segundos}s`; - - // Crear toast persistente con progreso - const progressToastId = showToast('info', 'Cambio Masivo de Estado en Progreso', - `Pasando ${procesosEnError.length} procesos a "En Espera"...\n⏱️ Tiempo estimado: ${tiempoTexto}`, - '', - true, // persistente - { current: 0, total: procesosEnError.length, percentage: 0 } - ); - - const inicioTiempo = Date.now(); - let exitosos = 0; - let errores = 0; - - for (let i = 0; i < procesosEnError.length; i++) { - const proc = procesosEnError[i]; - - // Actualizar progreso en tiempo real - const tiempoTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const progresoPercentage = Math.round(((i + 1) / procesosEnError.length) * 100); - const tiempoRestanteEstimado = i > 0 ? Math.round((tiempoTranscurrido / i) * (procesosEnError.length - i)) : tiempoEstimadoSegundos; - - updateToast(progressToastId, { - message: `Pasando ${procesosEnError.length} procesos a "En Espera"...\n⏱️ Transcurrido: ${tiempoTranscurrido}s | Restante: ~${tiempoRestanteEstimado}s\n📊 Proceso actual: ${proc.pedimento?.numero || proc.pedimento} (${i + 1}/${procesosEnError.length})`, - progress: { - current: i + 1, - total: procesosEnError.length, - percentage: progresoPercentage, - exitosos, - errores - } - }); - - try { - // Cambiar estado visual a "Procesando" temporalmente - updateProcesoEstado(proc.id, 2); - - const body = { - estado: 1, // Cambiar a En Espera - tipo_procesamiento: 2, - pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, - servicio: proc.servicio, - organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId - }; - - const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); - - if (!res.ok) { - // Si falla, revertir a estado Error - updateProcesoEstado(proc.id, 4); - errores++; - } else { - // Si es exitoso, cambiar estado a En Espera - updateProcesoEstado(proc.id, 1); - exitosos++; - } - - // Pequeña pausa entre requests para no sobrecargar - await new Promise(resolve => setTimeout(resolve, 300)); - } catch (err) { - updateProcesoEstado(proc.id, 4); - errores++; - } - } - - const tiempoTotalTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const minutosTotales = Math.floor(tiempoTotalTranscurrido / 60); - const segundosTotales = tiempoTotalTranscurrido % 60; - const tiempoTotalTexto = minutosTotales > 0 ? `${minutosTotales}m ${segundosTotales}s` : `${segundosTotales}s`; - - // Actualizar toast final - updateToast(progressToastId, { - type: 'success', - title: '✅ Cambio Masivo de Estado Completado', - message: `Resultados finales:\n✓ ${exitosos} exitosos | ✗ ${errores} errores\n⏱️ Tiempo total: ${tiempoTotalTexto}`, - progress: { - current: procesosEnError.length, - total: procesosEnError.length, - percentage: 100, - exitosos, - errores, - completed: true - } - }); - }; - - // Función para pasar seleccionados a En Espera - const handlePasarSeleccionadosAEspera = async () => { - if (selectedProcesos.length === 0) { - showToast('warning', 'Sin selección', 'No hay procesos seleccionados para cambiar.'); - return; - } - - const procesosACambiar = procesos.filter(proc => - selectedProcesos.includes(proc.id) && proc.estado === 4 - ); - - if (procesosACambiar.length === 0) { - showToast('warning', 'Sin procesos en error', 'Los procesos seleccionados no están en estado "Error".'); - return; - } - - // Calcular tiempo estimado (aproximadamente 1 segundo por proceso) - const tiempoEstimadoSegundos = procesosACambiar.length * 1; - const minutos = Math.floor(tiempoEstimadoSegundos / 60); - const segundos = Math.round(tiempoEstimadoSegundos % 60); - const tiempoTexto = minutos > 0 ? `${minutos}m ${segundos}s` : `${segundos}s`; - - // Crear toast persistente con progreso - const progressToastId = showToast('info', 'Cambio de Estado de Seleccionados en Progreso', - `Pasando ${procesosACambiar.length} procesos seleccionados a "En Espera"...\n⏱️ Tiempo estimado: ${tiempoTexto}`, - '', - true, // persistente - { current: 0, total: procesosACambiar.length, percentage: 0 } - ); - - const inicioTiempo = Date.now(); - let exitosos = 0; - let errores = 0; - - for (let i = 0; i < procesosACambiar.length; i++) { - const proc = procesosACambiar[i]; - - // Actualizar progreso en tiempo real - const tiempoTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const progresoPercentage = Math.round(((i + 1) / procesosACambiar.length) * 100); - const tiempoRestanteEstimado = i > 0 ? Math.round((tiempoTranscurrido / i) * (procesosACambiar.length - i)) : tiempoEstimadoSegundos; - - updateToast(progressToastId, { - message: `Pasando ${procesosACambiar.length} procesos seleccionados a "En Espera"...\n⏱️ Transcurrido: ${tiempoTranscurrido}s | Restante: ~${tiempoRestanteEstimado}s\n📊 Proceso actual: ${proc.pedimento?.numero || proc.pedimento} (${i + 1}/${procesosACambiar.length})`, - progress: { - current: i + 1, - total: procesosACambiar.length, - percentage: progresoPercentage, - exitosos, - errores - } - }); - - try { - // Cambiar estado visual a "Procesando" temporalmente - updateProcesoEstado(proc.id, 2); - - const body = { - estado: 1, // Cambiar a En Espera - tipo_procesamiento: 2, - pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, - servicio: proc.servicio, - organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId - }; - - const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); - - if (!res.ok) { - // Si falla, revertir a estado Error - updateProcesoEstado(proc.id, 4); - errores++; - } else { - // Si es exitoso, cambiar estado a En Espera - updateProcesoEstado(proc.id, 1); - exitosos++; - } - - // Pequeña pausa entre requests para no sobrecargar - await new Promise(resolve => setTimeout(resolve, 300)); - } catch (err) { - updateProcesoEstado(proc.id, 4); - errores++; - } - } - - // Limpiar selección después del cambio - setSelectedProcesos([]); - setIsSelectAll(false); - - const tiempoTotalTranscurrido = Math.round((Date.now() - inicioTiempo) / 1000); - const minutosTotales = Math.floor(tiempoTotalTranscurrido / 60); - const segundosTotales = tiempoTotalTranscurrido % 60; - const tiempoTotalTexto = minutosTotales > 0 ? `${minutosTotales}m ${segundosTotales}s` : `${segundosTotales}s`; - - // Actualizar toast final - updateToast(progressToastId, { - type: 'success', - title: '✅ Cambio de Estado de Seleccionados Completado', - message: `Resultados finales:\n✓ ${exitosos} exitosos | ✗ ${errores} errores\n⏱️ Tiempo total: ${tiempoTotalTexto}`, - progress: { - current: procesosACambiar.length, - total: procesosACambiar.length, - percentage: 100, - exitosos, - errores, - completed: true - } - }); - }; - - // Función para cambiar estado de Error a En Espera - const handlePasarAEspera = async (proc) => { - setChangingStateId(proc.id); - - // Cambiar estado visual a "Procesando" inmediatamente - updateProcesoEstado(proc.id, 2); // 2 = Procesando - - try { - const body = { - estado: 1, // Cambiar a En Espera - tipo_procesamiento: 2, - pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, - servicio: proc.servicio, - organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId - }; - - const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); - - if (!res.ok) { - // Si falla, revertir a estado Error - updateProcesoEstado(proc.id, 4); // 4 = Error - - let errorText = 'Error desconocido'; - try { - errorText = await res.text(); - } catch (textErr) { - // Error al leer respuesta - } - throw new Error(`Error al cambiar el estado del proceso: ${errorText}`); - } - - // Cambiar estado visual a "En Espera" si fue exitoso - updateProcesoEstado(proc.id, 1); // 1 = En Espera - - showToast('success', '¡Éxito!', 'Estado cambiado a "En Espera" correctamente'); - - // Refrescar la lista de procesos después de un delay más largo - setTimeout(() => { - window.location.reload(); - }, 3000); - } catch (err) { - // Crear un mensaje de error más detallado y persistente - const errorDetails = { - message: err.message, - status: err.status || 'N/A', - stack: err.stack, - timestamp: new Date().toISOString() - }; - - // Alert más detallado que permanece visible - const detailedMessage = ` -🚨 ERROR DETALLADO: -⏰ Tiempo: ${new Date().toLocaleString()} -📝 Mensaje: ${err.message} -🔢 Status: ${err.status || 'N/A'} -🔍 Tipo: ${err.name || 'Error'} - -📋 Copia este mensaje y compártelo para debugging. - `.trim(); - - if (err.message === 'SESSION_EXPIRED') { - showToast('error', '🚪 Sesión Expirada', 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', detailedMessage); - } else { - showToast('error', '🚨 Error al cambiar estado', 'Ocurrió un error al intentar cambiar el estado del proceso.', detailedMessage); - } - } finally { - setChangingStateId(null); - } - }; - - - // Función para ejecutar el servicio según el tipo de proceso - const handleEjecutarServicio = async (proc) => { - setExecutingId(proc.id); - - // Cambiar estado visual a "Procesando" inmediatamente - updateProcesoEstado(proc.id, 2); // 2 = Procesando - - let endpoint = ''; - // Determinar endpoint según el tipo de servicio - switch (proc.servicio) { - case 3: - endpoint = '/services/pedimento_completo'; - break; - case 4: // Partidas - endpoint = '/services/partidas'; - break; - case 5: // Remesas - endpoint = '/services/remesas'; - break; - case 6: // Acuse - endpoint = '/services/acuse'; - break; - case 7: - endpoint = '/services/edocument'; - break; - case 8: // Coves - endpoint = '/services/coves'; - break; - case 9: // Acuse Cove - endpoint = '/services/acuseCove'; - break; - default: - // Revertir estado si el servicio no es soportado - updateProcesoEstado(proc.id, proc.estado); // Revertir al estado original - showToast('error', 'Servicio no soportado', 'Este servicio no es compatible para ejecución directa.'); - setExecutingId(null); - return; - } - - try { - const body = { - pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, - organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId, - }; - - const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); - - if (!res.ok) { - // Si falla, cambiar estado a Error - updateProcesoEstado(proc.id, 4); // 4 = Error - throw new Error('Error al ejecutar el servicio'); - } - - // Si es exitoso, cambiar estado a Finalizado - updateProcesoEstado(proc.id, 3); // 3 = Finalizado - - showToast('success', '¡Servicio ejecutado!', 'El servicio se ha ejecutado correctamente'); - } catch (err) { - // Cambiar estado a Error en caso de excepción - updateProcesoEstado(proc.id, 4); // 4 = Error - - if (err.message === 'SESSION_EXPIRED') { - showToast('error', '🚪 Sesión Expirada', 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'); - } else { - showToast('error', '❌ Error al ejecutar servicio', 'Ocurrió un error al intentar ejecutar el servicio.', 'Error: ' + (err instanceof Error ? err.message : String(err))); - } - } finally { - setExecutingId(null); - } - }; + + // No se requieren estados ni funciones de acciones masivas useEffect(() => { - async function fetchProcesos() { + async function fetchData() { // Detectar si algún filtro cambió const currentFilters = { pedimentoPedimentoFilter, - estadoFilter, servicioFilter, + statusFilter, sortField, sortOrder }; @@ -729,15 +394,21 @@ export default function Procesos() { try { // Construir filtros const filters = {}; - if (pedimentoPedimentoFilter) filters['pedimento__pedimento_app'] = pedimentoPedimentoFilter; - if (estadoFilter) filters['estado'] = estadoFilter; + if (pedimentoPedimentoFilter) filters['pedimento_app'] = pedimentoPedimentoFilter; if (servicioFilter) filters['servicio'] = servicioFilter; + if (statusFilter) filters['status'] = statusFilter; if (sortField) { - filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + 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 fetchProcesamientoPedimentos(page, itemsPerPage, filters); - + const data = await fetchTasks(page, itemsPerPage, filters); setProcesos(data.results || []); setCount(data.count || 0); } catch (err) { @@ -750,11 +421,27 @@ export default function Procesos() { setLoading(false); } } - fetchProcesos(); - }, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]); + fetchData(); + }, [page, itemsPerPage, pedimentoPedimentoFilter, servicioFilter, statusFilter, sortField, sortOrder]); return (
+ {/* Modal de detalles del task */} + {selectedTask && ( + setSelectedTask(null)} + /> + )} + + {loadingTask && ( +
+
+
+
+
+ )} +
{/* Header mejorado y responsivo */}
- {/* Botones de acción masiva */} -
-

- - - - Acciones masivas -

- -
- {/* Sección de Procesamiento */} -
-
-
-

Ejecutar Servicios

-
-
- - -
-
- - {/* Divisor vertical */} -
- - {/* Sección de Cambio de Estado */} -
-
-
-

Pasar a En Espera

-
-
- - - -
-
- - {/* Divisor vertical */} -
- - {/* Sección de Control */} -
-
-
-

Control

-
- {selectedProcesos.length > 0 ? ( - - ) : ( -
- Sin selección -
- )} -
-
-
{/* Filtros responsivos mejorados */}
@@ -997,7 +532,7 @@ export default function Procesos() { Filtros de búsqueda -
+
+
-
+ +
-
- {/* Estados de carga y error mejorados */} +
+ {/* Estados de carga y error mejorados */} {loading ? (
@@ -1082,55 +628,47 @@ export default function Procesos() { style={{ position: 'relative', zIndex: 1 }}>
- - - - {procesos.length === 0 ? ( - ) : ( procesos.map((proc) => ( - + - - + - - - )) @@ -1286,32 +772,26 @@ export default function Procesos() { ) : ( procesos.map((proc) => ( -
+
- handleSelectProceso(proc.id, e.target.checked)} - disabled={proc.estado === 2 || proc.estado === 3} // Deshabilitar si está Procesando o Finalizado - className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 disabled:opacity-50 mt-1" - /> +
-

Proceso #{proc.id}

+

Proceso #{proc.task_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' }; + 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} @@ -1324,83 +804,44 @@ export default function Procesos() {
Pedimento: - - {typeof proc.pedimento === 'object' && proc.pedimento !== null - ? proc.pedimento.pedimento || JSON.stringify(proc.pedimento) - : proc.pedimento} - + {proc.pedimento_app || '-'}
- 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)} + Fecha: + + {new Date(proc.timestamp).toLocaleString('es-MX', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: true + })}
-
- {/* Botón Play - Ejecutar Servicio */} - - - {/* Botón Reload - Pasar a Espera */} - +
+ 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'} + + ); + })()}
)) @@ -1496,196 +937,7 @@ export default function Procesos() {
- {/* Sistema de Toast Notifications */} -
- {toasts.map((toast) => ( -
-
-
-
-
- {toast.type === 'success' ? ( - - - - ) : toast.type === 'error' ? ( - - - - ) : toast.type === 'warning' ? ( - - - - ) : ( - - - - )} -
-
-

{toast.title}

-

{toast.message}

- {toast.details && ( -
- - Ver detalles técnicos - -
-
{toast.details}
-
-
- )} - {/* Barra de progreso tipo tqdm */} - {toast.progress && ( -
-
- - {toast.progress.current}/{toast.progress.total} - ({toast.progress.percentage}%) - - {toast.progress.exitosos !== undefined && toast.progress.errores !== undefined && ( - - ✓{toast.progress.exitosos} - {toast.progress.errores > 0 && ( - ✗{toast.progress.errores} - )} - - )} -
-
-
- {/* Barra de progreso animada */} - {!toast.progress.completed && ( -
- )} -
-
- {/* Indicador de velocidad/rate (similar a tqdm) */} - {toast.progress.current > 0 && !toast.progress.completed && ( -
- 📈 {((toast.progress.current / (Date.now() - (toast.id || Date.now()))) * 1000 * 60).toFixed(1)} items/min -
- )} -
- )} -
- {/* Botón de cerrar - solo visible si es persistente o completado */} - {(toast.persistent || (toast.progress && toast.progress.completed)) && ( - - )} - {/* Indicador de toast automático */} - {!toast.persistent && (!toast.progress || !toast.progress.completed) && ( -
- - - -
- )} -
-
- - {/* Barra de progreso de tiempo (solo para toasts automáticos sin progreso personalizado) */} - {!toast.persistent && !toast.progress && ( -
-
-
- )} -
-
- ))} -
- {/* Estilos CSS para las animaciones */} -
); }
- {pedimento.pedimento_app} + {pedimento.pedimento_app} - - - -
+
+ handleSelectProceso(proceso.task_id, e.target.checked)} + disabled={proceso.status === 'running' || proceso.status === 'completed'} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded disabled:opacity-50" + /> +
+
@@ -3622,7 +3738,10 @@ export default function PedimentoDetail() {
- #{proceso.id} + #{proceso.task_id} +
+
+ {proceso.pedimento_app}
@@ -3633,9 +3752,17 @@ export default function PedimentoDetail() {
- - {getEstadoLabel(proceso.estado)} + + {getTaskStatusLabel(proceso.status)} + {proceso.status === 'running' && ( + + + + + + + )} {proceso.organizacion_name || 'N/A'} @@ -3649,81 +3776,58 @@ export default function PedimentoDetail() {
{/* Botón Play (Ejecutar Servicio) */} - + {isTaskActionable(proceso.status) && ( + + )} - {/* Botón Refresh (Pasar a Espera) */} - + {/* Botón Retry (Reintentar) */} + {proceso.status === 'failed' && ( + + )}
- - { - setSortField('id'); - setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc'); + setSortField('task_id'); + setSortOrder(sortField === 'task_id' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
- ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')} + Task ID {sortField === 'task_id' && (sortOrder === 'asc' ? '▲' : '▼')}
{ - setSortField('organizacion_name'); - setSortOrder(sortField === 'organizacion_name' && sortOrder === 'asc' ? 'desc' : 'asc'); + setSortField('pedimento_app'); + setSortOrder(sortField === 'pedimento_app' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
- Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')} + Pedimento {sortField === 'pedimento_app' && (sortOrder === 'asc' ? '▲' : '▼')}
{ - setSortField('estado'); - setSortOrder(sortField === 'estado' && sortOrder === 'asc' ? 'desc' : 'asc'); + setSortField('status'); + setSortOrder(sortField === 'status' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
- Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')} + Estado {sortField === 'status' && (sortOrder === 'asc' ? '▲' : '▼')}
{ - setSortField('pedimento'); - setSortOrder(sortField === 'pedimento' && sortOrder === 'asc' ? 'desc' : 'asc'); + setSortField('timestamp'); + setSortOrder(sortField === 'timestamp' && sortOrder === 'asc' ? 'desc' : 'asc'); }} >
- Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')} + Fecha de creación {sortField === 'timestamp' && (sortOrder === 'asc' ? '▲' : '▼')}
{ setSortField('servicio'); setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc'); @@ -1140,15 +678,12 @@ export default function Procesos() { Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')} - Acciones -
+
@@ -1162,27 +697,23 @@ export default function Procesos() {
- handleSelectProceso(proc.id, e.target.checked)} - disabled={proc.estado === 2 || proc.estado === 3} // Deshabilitar si está Procesando o Finalizado - className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 disabled:opacity-50" - /> + - {proc.id} - {proc.organizacion_name || '-'}{proc.pedimento_app || '-'} {(() => { - 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' }; + 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} @@ -1190,80 +721,35 @@ export default function Procesos() { ); })()} - - {typeof proc.pedimento === 'object' && proc.pedimento !== null - ? proc.pedimento.pedimento_app || proc.pedimento.pedimento_app || JSON.stringify(proc.pedimento) - : proc.pedimento} - + + {new Date(proc.timestamp).toLocaleString('es-MX', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: true + })} - {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)} - -
- {/* Botón Play - Ejecutar Servicio */} - - - {/* Botón Reload - Pasar a Espera */} - -
+
+ {(() => { + 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'} + + ); + })()}