Files
frontend/src/pages/Procesos.jsx

386 lines
21 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 } from 'react';
import { fetchProcesamientoPedimentos } from '../api/procesos.ts';
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('');
// Estado para loading de ejecución de servicio
const [executingId, setExecutingId] = useState(null);
// Dropdown state: id del proceso abierto o null
const [openDropdownId, setOpenDropdownId] = useState(null);
// Función para ejecutar el servicio según el tipo de proceso
const handleEjecutarServicio = async (proc) => {
setExecutingId(proc.id);
let endpoint = '';
// Determinar endpoint según el tipo de servicio
switch (proc.servicio) {
case 4: // Partidas
endpoint = '/services/partidas';
break;
case 5: // Remesas
endpoint = '/services/remesas';
break;
case 6: // Acuse
endpoint = '/services/acuse';
break;
case 8: // Acuse Cove
endpoint = '/services/acuseCove';
break;
default:
alert('Servicio no soportado para ejecución directa.');
setExecutingId(null);
return;
}
try {
const token = localStorage.getItem('access');
const headers = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
};
const body = JSON.stringify({
pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento,
organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId,
});
const res = await fetch(`${MICROSERVICE_URL}${endpoint}`, {
method: 'POST',
headers,
body,
});
if (!res.ok) throw new Error('Error al ejecutar el servicio');
alert('Servicio ejecutado correctamente');
setOpenDropdownId(null);
} catch (err) {
alert('Error al ejecutar el servicio: ' + (err instanceof Error ? err.message : String(err)));
} finally {
setExecutingId(null);
}
};
// Cierra el dropdown si se hace click fuera
useEffect(() => {
if (openDropdownId === null) return;
function handleClick(e) {
const el = document.getElementById(`dropdown-acciones-${openDropdownId}`);
if (el && !el.contains(e.target)) {
setOpenDropdownId(null);
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [openDropdownId]);
useEffect(() => {
async function fetchProcesos() {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('access');
// Construir query params
const params = new URLSearchParams();
params.append('page', String(page));
params.append('page_size', String(itemsPerPage));
if (pedimentoPedimentoFilter) params.append('pedimento__pedimento', pedimentoPedimentoFilter);
if (estadoFilter) params.append('estado', estadoFilter);
if (servicioFilter) params.append('servicio', servicioFilter);
// ...existing code...
const API_URL = import.meta.env.VITE_EFC_API_URL;
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`, { headers });
if (!res.ok) throw new Error('Error al obtener procesamiento de pedimentos');
const data = await res.json();
setProcesos(data.results || []);
setCount(data.count || 0);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
}
fetchProcesos();
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter]);
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="max-w-7xl mx-auto">
<div className="mb-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-50 via-white to-indigo-50 border border-blue-100 p-8 flex items-center 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-blue-100 rounded-full p-4 shadow-md animate-bounce-slow">
<svg className="h-10 w-10 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>
<h1 className="text-3xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Procesos del Sistema
</h1>
<p className="text-lg text-blue-700/80 font-medium">Estado actual de los procesos de la agencia aduanal</p>
</div>
<div className="absolute -top-10 -right-10 opacity-30 pointer-events-none select-none">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" fill="url(#grad1)" />
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
<stop stopColor="#3b82f6" stopOpacity="0.15" />
<stop offset="1" stopColor="#6366f1" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
</div>
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s 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;
}
`}</style>
</div>
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
<h2 className="text-2xl font-bold text-blue-800 mb-6">Procesamiento de Pedimentos</h2>
{/* Filtros */}
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
<div className="flex flex-col flex-1 min-w-[150px]">
<label className="text-xs font-semibold text-gray-700 mb-1">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-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
/>
</div>
<div className="flex flex-col flex-1 min-w-[150px]">
<label className="text-xs font-semibold text-gray-700 mb-1">Estado</label>
<select
value={estadoFilter}
onChange={e => setEstadoFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
>
<option value="">Todos</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="flex flex-col flex-1 min-w-[150px]">
<label className="text-xs font-semibold text-gray-700 mb-1">Servicio</label>
<select
value={servicioFilter}
onChange={e => setServicioFilter(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50"
>
<option value="">Todos</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>
</select>
</div>
{/* ...filtros anteriores... */}
</div>
{loading ? (
<div className="text-center text-gray-500 py-8">Cargando procesos...</div>
) : error ? (
<div className="text-center text-danger-600 py-8">{error}</div>
) : (
<div className="overflow-x-auto">
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: 'auto', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
<tr>
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">ID</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Organización</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Estado</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Pedimento</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Servicio</th>
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Acciones</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(12 * 40px)' }}>
{procesos.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-8 text-gray-500">No hay registros</td>
</tr>
) : (
procesos.map((proc) => (
<tr key={proc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">{proc.id}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{proc.organizacion_name || '-'}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{
proc.estado === 1 ? 'En Espera'
: proc.estado === 2 ? 'Procesando'
: proc.estado === 3 ? 'Finalizado'
: proc.estado === 4 ? 'Error'
: String(proc.estado)
}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{
typeof proc.pedimento === 'object' && proc.pedimento !== null
? proc.pedimento.pedimento || JSON.stringify(proc.pedimento)
: proc.pedimento
}</td>
<td className="px-2 py-2 whitespace-nowrap align-middle">{
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'
: String(proc.servicio)
}</td>
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">
<div className="relative inline-block text-left" id={`dropdown-acciones-${proc.id}`}>
<button
className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-3 py-1 bg-white text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
type="button"
onClick={() => setOpenDropdownId(openDropdownId === proc.id ? null : proc.id)}
>
Acciones
<svg className="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
</button>
{openDropdownId === proc.id && (
<div className="absolute right-0 mt-2 w-32 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-20">
<div className="py-1">
<button
className="block w-full text-left px-4 py-2 text-xs text-blue-700 hover:bg-blue-100 disabled:opacity-60"
onClick={() => handleEjecutarServicio(proc)}
disabled={executingId === proc.id}
>
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
</button>
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Pasar a espera</button>
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Editar</button>
</div>
</div>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Paginación igual a Documents.jsx */}
{count > 0 && (
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
{(() => {
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 flex-col sm:flex-row sm:items-center w-full gap-2 sm:gap-4 mt-2 sm:mt-0">
<div className="flex items-center gap-2">
<label htmlFor="itemsPerPage" className="text-xs 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 px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
>
{[5, 8, 12, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center gap-1 flex-wrap">
<button
type="button"
onClick={e => { e.preventDefault(); setPage(1); }}
disabled={page === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
«
</button>
<button
type="button"
onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }}
disabled={page === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${page === 1 ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
</button>
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => { e.preventDefault(); setPage(num); }}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === page ? 'bg-blue-600 text-white border-blue-700 cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
disabled={num === page}
>
{num}
</button>
))}
<button
type="button"
onClick={e => { e.preventDefault(); setPage(p => p + 1); }}
disabled={page >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
</button>
<button
type="button"
onClick={e => { e.preventDefault(); setPage(totalPages); }}
disabled={page >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(page >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900'}`}
>
»
</button>
<span className="ml-3 text-xs text-gray-500">Página <span className="font-bold">{page}</span> de <span className="font-bold">{totalPages}</span></span>
</div>
</div>
);
})()}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}