Files
frontend/src/pages/Procesos.jsx
JCedillo b35c87bd28 feat: Add checkbox selection and bulk operations
- Add checkbox functionality to Expedientes.jsx with bulk document deletion
- Add checkbox functionality to PedimentoDetail.jsx documents table
- Implement custom modal for deletion confirmation with modern UI
- Fix pagination reset on filter changes in Procesos.jsx
- Add bulk selection with 'select all' functionality
- Integrate with /record/documents/bulk-delete/ endpoint
- Improve UX with loading states and success/error messages
2025-10-09 13:01:49 -05:00

1692 lines
86 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 { 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;
// Estado para loading de ejecución de servicio
// y función para ejecutar el servicio según el tipo de proceso
export default function Procesos() {
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);
// Filtros
const [pedimentoPedimentoFilter, setPedimentoPedimentoFilter] = useState('');
const [estadoFilter, setEstadoFilter] = useState('');
const [servicioFilter, setServicioFilter] = 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: '',
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);
}
};
useEffect(() => {
async function fetchProcesos() {
// Detectar si algún filtro cambió
const currentFilters = {
pedimentoPedimentoFilter,
estadoFilter,
servicioFilter,
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__pedimento_app'] = pedimentoPedimentoFilter;
if (estadoFilter) filters['estado'] = estadoFilter;
if (servicioFilter) filters['servicio'] = servicioFilter;
if (sortField) {
filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + sortField;
}
const data = await fetchProcesamientoPedimentos(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);
}
}
fetchProcesos();
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter, sortField, sortOrder]);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto">
{/* Header mejorado y responsivo */}
<div className="mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex items-center gap-4 sm:gap-6 animate-fadein-slideup opacity-0"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
<span>Procesos del Sistema</span>
{count > 0 && (
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs sm:text-sm font-semibold px-3 py-1 rounded-full shadow-lg animate-fade-in">
{count} procesos
</span>
)}
</h1>
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Estado actual de los procesos de la agencia aduanal</p>
</div>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
</div>
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
</div>
{/* Animaciones CSS */}
<style>{`
@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="bg-white rounded-3xl shadow-2xl border border-gray-100 p-4 sm:p-6 lg:p-8 animate-fadein-slideup opacity-0"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl p-2 shadow-lg">
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
Procesamiento de Pedimentos
</h2>
<div className="flex flex-col sm:flex-row gap-3">
{count > 0 && (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl px-4 py-2 border border-blue-100">
<span className="text-sm font-medium text-blue-700">Total de registros: </span>
<span className="text-lg font-bold text-blue-800">{count}</span>
</div>
)}
</div>
</div>
{/* Botones de acción masiva */}
<div className="mb-6 bg-gradient-to-r from-emerald-50 to-green-50 rounded-2xl p-4 sm:p-6 border border-emerald-200">
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Acciones masivas
</h3>
<div className="flex flex-col lg:flex-row gap-4">
{/* Sección de Procesamiento */}
<div className="flex-1 bg-white rounded-xl p-4 border border-green-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<h4 className="text-sm font-semibold text-gray-700">Ejecutar Servicios</h4>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={handleProcesarPaginaEntera}
disabled={procesos.filter(proc => proc.estado === 1 || proc.estado === 4).length === 0}
className={`flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
procesos.filter(proc => proc.estado === 1 || proc.estado === 4).length === 0
? 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200'
: 'bg-gradient-to-r from-emerald-500 to-green-600 text-white shadow-md hover:shadow-lg focus:ring-emerald-500'
}`}
>
<svg className="w-4 h-4" 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>
Página Entera
{procesos.filter(proc => proc.estado === 1 || proc.estado === 4).length > 0 && (
<span className="bg-white/20 px-1.5 py-0.5 rounded-full text-xs">
{procesos.filter(proc => proc.estado === 1 || proc.estado === 4).length}
</span>
)}
</button>
<button
onClick={handleProcesarSeleccionados}
disabled={selectedProcesos.length === 0}
className={`flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
selectedProcesos.length === 0
? 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200'
: 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-md hover:shadow-lg focus:ring-blue-500'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Seleccionados
{selectedProcesos.length > 0 && (
<span className="bg-white/20 px-1.5 py-0.5 rounded-full text-xs">
{selectedProcesos.length}
</span>
)}
</button>
</div>
</div>
{/* Divisor vertical */}
<div className="hidden lg:block w-px bg-gray-300"></div>
{/* Sección de Cambio de Estado */}
<div className="flex-1 bg-white rounded-xl p-4 border border-orange-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-3 h-3 bg-orange-500 rounded-full"></div>
<h4 className="text-sm font-semibold text-gray-700">Pasar a En Espera</h4>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={handlePasarPaginaAEspera}
disabled={procesos.filter(proc => proc.estado === 4).length === 0}
className={`flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
procesos.filter(proc => proc.estado === 4).length === 0
? 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200'
: 'bg-gradient-to-r from-orange-500 to-red-500 text-white shadow-md hover:shadow-lg focus:ring-orange-500'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Página Entera
{procesos.filter(proc => proc.estado === 4).length > 0 && (
<span className="bg-white/20 px-1.5 py-0.5 rounded-full text-xs">
{procesos.filter(proc => proc.estado === 4).length}
</span>
)}
</button>
<button
onClick={handlePasarSeleccionadosAEspera}
disabled={selectedProcesos.filter(id => {
const proc = procesos.find(p => p.id === id);
return proc && proc.estado === 4;
}).length === 0}
className={`flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
selectedProcesos.filter(id => {
const proc = procesos.find(p => p.id === id);
return proc && proc.estado === 4;
}).length === 0
? 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200'
: 'bg-gradient-to-r from-yellow-500 to-orange-500 text-white shadow-md hover:shadow-lg focus:ring-yellow-500'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Seleccionados
{selectedProcesos.filter(id => {
const proc = procesos.find(p => p.id === id);
return proc && proc.estado === 4;
}).length > 0 && (
<span className="bg-white/20 px-1.5 py-0.5 rounded-full text-xs">
{selectedProcesos.filter(id => {
const proc = procesos.find(p => p.id === id);
return proc && proc.estado === 4;
}).length}
</span>
)}
</button>
</div>
</div>
{/* Divisor vertical */}
<div className="hidden lg:block w-px bg-gray-300"></div>
{/* Sección de Control */}
<div className="lg:w-auto bg-white rounded-xl p-4 border border-gray-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-3 h-3 bg-gray-500 rounded-full"></div>
<h4 className="text-sm font-semibold text-gray-700">Control</h4>
</div>
{selectedProcesos.length > 0 ? (
<button
onClick={() => {
setSelectedProcesos([]);
setIsSelectAll(false);
}}
className="flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200 bg-gray-100 text-gray-600 hover:bg-gray-200 border border-gray-300 w-full"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Limpiar
</button>
) : (
<div className="flex items-center justify-center px-4 py-2 text-sm text-gray-400">
Sin selección
</div>
)}
</div>
</div>
</div>
{/* Filtros responsivos mejorados */}
<div className="mb-6 bg-gradient-to-r from-gray-50 to-slate-50 rounded-2xl p-4 sm:p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-800 mb-4 flex items-center gap-2">
<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 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<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)}
placeholder="Buscar por pedimento..."
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
Estado
</label>
<select
value={estadoFilter}
onChange={e => setEstadoFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
>
<option value="">Todos los estados</option>
<option value="1">En Espera</option>
<option value="2">Procesando</option>
<option value="3">Finalizado</option>
<option value="4">Error</option>
</select>
</div>
<div className="space-y-2 sm:col-span-2 lg:col-span-1">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
Servicio
</label>
<select
value={servicioFilter}
onChange={e => setServicioFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
>
<option value="">Todos los servicios</option>
<option value="1">Estado de pedimento</option>
<option value="2">Listado de pedimentos</option>
<option value="3">Pedimento Completo</option>
<option value="4">Pedimento Partidas</option>
<option value="5">Pedimento Remesas</option>
<option value="6">Acuse</option>
<option value="7">EDocument</option>
<option value="8">Cove</option>
<option value="9">Acuse Cove</option>
</select>
</div>
</div>
</div>
{/* Estados de carga y error mejorados */}
{loading ? (
<div className="flex flex-col items-center justify-center py-12">
<div className="relative">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600"></div>
<div className="absolute inset-0 bg-blue-500/10 rounded-full blur-xl animate-pulse"></div>
</div>
<p className="mt-4 text-gray-600 font-medium">Cargando procesos...</p>
</div>
) : error ? (
<div className="bg-red-50 border border-red-200 rounded-2xl p-6 text-center">
<div className="bg-red-100 rounded-full p-3 w-12 h-12 mx-auto mb-4 flex items-center justify-center">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-red-800 mb-2">Error al cargar</h3>
<p className="text-red-600">{error}</p>
</div>
) : (
<>
{/* Vista de tabla para pantallas grandes */}
<div className="hidden lg:block overflow-x-auto bg-white rounded-2xl border border-gray-200 shadow-sm relative pb-20"
style={{
overflowY: 'visible' // Permitir que los dropdowns se muestren fuera del contenedor
}}>
<table className="min-w-full divide-y divide-gray-300 relative"
style={{ position: 'relative', zIndex: 1 }}>
<thead className="bg-gray-50 sticky top-0 z-10">
<tr>
<th scope="col" className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider rounded-tl-2xl">
<input
type="checkbox"
checked={isSelectAll}
onChange={handleSelectAll}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
/>
</th>
<th className="px-4 py-4 text-center text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('id');
setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center justify-center gap-1">
ID {sortField === 'id' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('organizacion_name');
setSortOrder(sortField === 'organizacion_name' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Organización {sortField === 'organizacion_name' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('estado');
setSortOrder(sortField === 'estado' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Estado {sortField === 'estado' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('pedimento');
setSortOrder(sortField === 'pedimento' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Pedimento {sortField === 'pedimento' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200"
onClick={() => {
setSortField('servicio');
setSortOrder(sortField === 'servicio' && sortOrder === 'asc' ? 'desc' : 'asc');
}}
>
<div className="flex items-center gap-1">
Servicio {sortField === 'servicio' && (sortOrder === 'asc' ? '▲' : '▼')}
</div>
</th>
<th className="px-4 py-4 text-center text-xs font-bold text-gray-600 uppercase tracking-wider rounded-tr-2xl">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100 relative" style={{ position: 'relative' }}>
{procesos.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-12">
<div className="flex flex-col items-center">
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<p className="text-gray-500 font-medium">No hay procesos disponibles</p>
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
</div>
</td>
</tr>
) : (
procesos.map((proc) => (
<tr key={proc.id} className="transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 hover:shadow-lg">
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
<input
type="checkbox"
checked={selectedProcesos.includes(proc.id)}
onChange={(e) => 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"
/>
</td>
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
<span className="bg-gray-100 text-gray-800 px-2 py-1 rounded-lg text-sm font-semibold">{proc.id}</span>
</td>
<td className="px-4 py-4 whitespace-nowrap align-middle text-sm font-medium text-gray-900">{proc.organizacion_name || '-'}</td>
<td className="px-4 py-4 whitespace-nowrap align-middle">
{(() => {
const estado = proc.estado === 1 ? { text: 'En Espera', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' }
: proc.estado === 2 ? { text: 'Procesando', color: 'bg-blue-100 text-blue-800 border-blue-200' }
: proc.estado === 3 ? { text: 'Finalizado', color: 'bg-green-100 text-green-800 border-green-200' }
: proc.estado === 4 ? { text: 'Error', color: 'bg-red-100 text-red-800 border-red-200' }
: { text: String(proc.estado), color: 'bg-gray-100 text-gray-800 border-gray-200' };
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold border ${estado.color}`}>
{estado.text}
</span>
);
})()}
</td>
<td className="px-4 py-4 whitespace-nowrap align-middle text-sm text-gray-900 font-mono">
<Link
to={`/expedientes/pedimento/${typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id || proc.pedimento.pedimento_app : proc.pedimento}`}
className="hover:text-blue-600 hover:underline"
>
{typeof proc.pedimento === 'object' && proc.pedimento !== null
? proc.pedimento.pedimento_app || proc.pedimento.pedimento_app || JSON.stringify(proc.pedimento)
: proc.pedimento}
</Link>
</td>
<td className="px-4 py-4 whitespace-nowrap align-middle text-sm text-gray-700">
{proc.servicio === 1 ? 'Estado de pedimento'
: proc.servicio === 2 ? 'Listado de pedimentos'
: proc.servicio === 3 ? 'Pedimento Completo'
: proc.servicio === 4 ? 'Pedimento Partidas'
: proc.servicio === 5 ? 'Pedimento Remesas'
: proc.servicio === 6 ? 'Acuse'
: proc.servicio === 7 ? 'EDocument'
: proc.servicio === 8 ? 'Cove'
: proc.servicio === 9 ? 'Acuse Cove'
: String(proc.servicio)}
</td>
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
<div className="flex items-center justify-center gap-2">
{/* Botón Play - Ejecutar Servicio */}
<button
className={`group inline-flex items-center justify-center w-9 h-9 rounded-lg border transition-all duration-200 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
executingId === proc.id ||
proc.estado === 3 || // Deshabilitar si está Finalizado
proc.estado === 2 // Deshabilitar si está Procesando
? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50'
: 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'
}`}
onClick={() => handleEjecutarServicio(proc)}
disabled={
executingId === proc.id ||
proc.estado === 3 || // Deshabilitar si está Finalizado
proc.estado === 2 // Deshabilitar si está Procesando
}
title={executingId === proc.id ? 'Ejecutando...' : proc.estado === 2 ? 'Proceso en ejecución' : 'Ejecutar Servicio'}
>
{executingId === proc.id ? (
<svg className="w-4 h-4 text-gray-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
) : (
<svg className="w-4 h-4 text-green-600 group-hover:text-green-700" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</button>
{/* Botón Reload - Pasar a Espera */}
<button
className={`group inline-flex items-center justify-center w-9 h-9 rounded-lg border transition-all duration-200 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
proc.estado !== 4 || changingStateId === proc.id
? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50'
: 'bg-orange-50 border-orange-200 hover:bg-orange-100 hover:border-orange-300 focus:ring-orange-500 cursor-pointer'
}`}
disabled={proc.estado !== 4 || changingStateId === proc.id}
onClick={() => handlePasarAEspera(proc)}
title={changingStateId === proc.id ? 'Cambiando...' : 'Pasar a Espera'}
>
{changingStateId === proc.id ? (
<svg className="w-4 h-4 text-gray-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
) : (
<svg className="w-4 h-4 text-orange-600 group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)}
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="lg:hidden space-y-4">
{procesos.length === 0 ? (
<div className="bg-gray-50 rounded-2xl p-8 text-center">
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<p className="text-gray-500 font-medium">No hay procesos disponibles</p>
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
</div>
) : (
procesos.map((proc) => (
<div key={proc.id} className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 hover:shadow-xl transition-all duration-300">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={selectedProcesos.includes(proc.id)}
onChange={(e) => 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"
/>
<div className="bg-blue-100 rounded-xl p-2 flex-shrink-0">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-lg font-semibold text-gray-900">Proceso #{proc.id}</h3>
<p className="text-sm text-gray-500">{proc.organizacion_name || 'Sin organización'}</p>
</div>
</div>
{(() => {
const estado = proc.estado === 1 ? { text: 'En Espera', color: 'bg-yellow-100 text-yellow-800 border-yellow-200' }
: proc.estado === 2 ? { text: 'Procesando', color: 'bg-blue-100 text-blue-800 border-blue-200' }
: proc.estado === 3 ? { text: 'Finalizado', color: 'bg-green-100 text-green-800 border-green-200' }
: proc.estado === 4 ? { text: 'Error', color: 'bg-red-100 text-red-800 border-red-200' }
: { text: String(proc.estado), color: 'bg-gray-100 text-gray-800 border-gray-200' };
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold border ${estado.color}`}>
{estado.text}
</span>
);
})()}
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Pedimento:</span>
<span className="text-sm font-mono text-gray-900 bg-gray-100 px-2 py-1 rounded">
<Link
to={`/expedientes/pedimento/${typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id || proc.pedimento.pedimento_app : proc.pedimento}`}
className="hover:text-blue-600 hover:underline"
>
{typeof proc.pedimento === 'object' && proc.pedimento !== null
? proc.pedimento.pedimento || JSON.stringify(proc.pedimento)
: proc.pedimento}
</Link>
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Servicio:</span>
<span className="text-sm text-gray-900 text-right max-w-[60%]">
{proc.servicio === 1 ? 'Estado de pedimento'
: proc.servicio === 2 ? 'Listado de pedimentos'
: proc.servicio === 3 ? 'Pedimento Completo'
: proc.servicio === 4 ? 'Pedimento Partidas'
: proc.servicio === 5 ? 'Pedimento Remesas'
: proc.servicio === 6 ? 'Acuse'
: proc.servicio === 7 ? 'EDocument'
: proc.servicio === 8 ? 'Cove'
: proc.servicio === 9 ? 'Acuse Cove'
: String(proc.servicio)}
</span>
</div>
</div>
<div className="flex items-center justify-center gap-3">
{/* Botón Play - Ejecutar Servicio */}
<button
className={`group inline-flex items-center justify-center w-10 h-10 rounded-xl border transition-all duration-200 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
executingId === proc.id ||
proc.estado === 3 || // Deshabilitar si está Finalizado
proc.estado === 2 // Deshabilitar si está Procesando
? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50'
: 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer shadow-sm hover:shadow-md'
}`}
onClick={() => handleEjecutarServicio(proc)}
disabled={
executingId === proc.id ||
proc.estado === 3 || // Deshabilitar si está Finalizado
proc.estado === 2 // Deshabilitar si está Procesando
}
title={executingId === proc.id ? 'Ejecutando...' : proc.estado === 2 ? 'Proceso en ejecución' : 'Ejecutar Servicio'}
>
{executingId === proc.id ? (
<svg className="w-5 h-5 text-gray-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
) : (
<svg className="w-5 h-5 text-green-600 group-hover:text-green-700" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</button>
{/* Botón Reload - Pasar a Espera */}
<button
className={`group inline-flex items-center justify-center w-10 h-10 rounded-xl border transition-all duration-200 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
proc.estado !== 4 || changingStateId === proc.id
? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50'
: 'bg-orange-50 border-orange-200 hover:bg-orange-100 hover:border-orange-300 focus:ring-orange-500 cursor-pointer shadow-sm hover:shadow-md'
}`}
disabled={proc.estado !== 4 || changingStateId === proc.id}
onClick={() => handlePasarAEspera(proc)}
title={changingStateId === proc.id ? 'Cambiando...' : 'Pasar a Espera'}
>
{changingStateId === proc.id ? (
<svg className="w-5 h-5 text-gray-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
) : (
<svg className="w-5 h-5 text-orange-600 group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)}
</button>
</div>
</div>
))
)}
</div>
{/* Paginación compartida mejorada */}
{count > 0 && (
<div className="bg-gradient-to-r from-gray-50 to-slate-50 px-4 sm:px-6 py-4 mt-6 rounded-2xl border border-gray-200 flex flex-col sm:flex-row items-center justify-between gap-4">
{(() => {
const totalPages = Math.max(1, Math.ceil(count / itemsPerPage));
const 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 text-gray-600 font-medium">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => { setItemsPerPage(Number(e.target.value)); setPage(1); }}
className="border border-gray-300 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm"
>
{[5, 8, 12, 20, 50, 100].map(size => (
<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>
{/* Sistema de Toast Notifications */}
<div className="fixed top-4 right-4 z-50 space-y-3 max-w-sm">
{toasts.map((toast) => (
<div
key={toast.id}
className={`transform transition-all duration-500 ease-in-out ${
toast.isVisible
? 'translate-x-0 opacity-100 scale-100'
: 'translate-x-full opacity-0 scale-95'
}`}
>
<div className={`rounded-xl shadow-2xl border-l-4 backdrop-blur-sm overflow-hidden ${
toast.type === 'success'
? 'bg-green-50/95 border-green-500 text-green-800'
: toast.type === 'error'
? 'bg-red-50/95 border-red-500 text-red-800'
: toast.type === 'warning'
? 'bg-orange-50/95 border-orange-500 text-orange-800'
: toast.type === 'info'
? 'bg-blue-50/95 border-blue-500 text-blue-800'
: 'bg-blue-50/95 border-blue-500 text-blue-800'
}`}>
<div className="p-4">
<div className="flex items-start gap-3">
<div className={`flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center ${
toast.type === 'success'
? 'bg-green-500'
: toast.type === 'error'
? 'bg-red-500'
: toast.type === 'warning'
? 'bg-orange-500'
: toast.type === 'info'
? 'bg-blue-500'
: 'bg-blue-500'
}`}>
{toast.type === 'success' ? (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
) : toast.type === 'error' ? (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
) : toast.type === 'warning' ? (
<svg className="w-4 h-4 text-white" 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>
) : (
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
</div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold mb-1">{toast.title}</h4>
<p className="text-sm opacity-90 whitespace-pre-line">{toast.message}</p>
{toast.details && (
<details className="mt-2 cursor-pointer">
<summary className="text-xs opacity-70 hover:opacity-100 transition-opacity">
Ver detalles técnicos
</summary>
<div className={`mt-1 p-2 rounded text-xs font-mono text-left max-h-20 overflow-y-auto ${
toast.type === 'success'
? 'bg-green-100/80'
: toast.type === 'error'
? 'bg-red-100/80'
: toast.type === 'warning'
? 'bg-orange-100/80'
: toast.type === 'info'
? 'bg-blue-100/80'
: 'bg-blue-100/80'
}`}>
<pre className="whitespace-pre-wrap break-words">{toast.details}</pre>
</div>
</details>
)}
{/* Barra de progreso tipo tqdm */}
{toast.progress && (
<div className="mt-3">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-medium">
{toast.progress.current}/{toast.progress.total}
({toast.progress.percentage}%)
</span>
{toast.progress.exitosos !== undefined && toast.progress.errores !== undefined && (
<span className="text-xs">
<span className="text-green-600">{toast.progress.exitosos}</span>
{toast.progress.errores > 0 && (
<span className="text-red-600 ml-1">{toast.progress.errores}</span>
)}
</span>
)}
</div>
<div className={`w-full bg-gray-200 rounded-full h-2 overflow-hidden ${
toast.type === 'success' ? 'bg-green-100' :
toast.type === 'error' ? 'bg-red-100' :
toast.type === 'warning' ? 'bg-orange-100' :
'bg-blue-100'
}`}>
<div
className={`h-2 rounded-full transition-all duration-300 ease-out ${
toast.type === 'success' ? 'bg-green-500' :
toast.type === 'error' ? 'bg-red-500' :
toast.type === 'warning' ? 'bg-orange-500' :
'bg-blue-500'
}`}
style={{ width: `${toast.progress.percentage}%` }}
>
{/* Barra de progreso animada */}
{!toast.progress.completed && (
<div className="h-full w-full bg-gradient-to-r from-transparent via-white to-transparent opacity-30 animate-pulse"></div>
)}
</div>
</div>
{/* Indicador de velocidad/rate (similar a tqdm) */}
{toast.progress.current > 0 && !toast.progress.completed && (
<div className="text-xs opacity-70 mt-1">
📈 {((toast.progress.current / (Date.now() - (toast.id || Date.now()))) * 1000 * 60).toFixed(1)} items/min
</div>
)}
</div>
)}
</div>
{/* Botón de cerrar - solo visible si es persistente o completado */}
{(toast.persistent || (toast.progress && toast.progress.completed)) && (
<button
onClick={() => removeToast(toast.id)}
className="flex-shrink-0 ml-2 opacity-60 hover:opacity-100 transition-opacity p-1 rounded-full hover:bg-black/10"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
{/* Indicador de toast automático */}
{!toast.persistent && (!toast.progress || !toast.progress.completed) && (
<div className="flex-shrink-0 ml-2 opacity-40">
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
)}
</div>
</div>
{/* Barra de progreso de tiempo (solo para toasts automáticos sin progreso personalizado) */}
{!toast.persistent && !toast.progress && (
<div className={`h-1 ${
toast.type === 'success'
? 'bg-green-200'
: toast.type === 'error'
? 'bg-red-200'
: toast.type === 'warning'
? 'bg-orange-200'
: toast.type === 'info'
? 'bg-blue-200'
: 'bg-blue-200'
}`}>
<div
className={`h-full transition-all ease-linear ${
toast.type === 'success'
? 'bg-green-500'
: toast.type === 'error'
? 'bg-red-500'
: toast.type === 'warning'
? 'bg-orange-500'
: toast.type === 'info'
? 'bg-blue-500'
: 'bg-blue-500'
}`}
style={{
width: '100%',
animationDuration: toast.type === 'info' ? '8000ms' : '5000ms',
animation: `toast-progress ${toast.type === 'info' ? '8s' : '5s'} linear forwards`
}}
/>
</div>
)}
</div>
</div>
))}
</div>
{/* Estilos CSS para las animaciones */}
<style>{`
@keyframes toast-progress {
from { width: 100%; }
to { width: 0%; }
}
`}</style>
</div>
);
}