Primera version de frontend
This commit is contained in:
385
src/pages/Procesos.jsx
Normal file
385
src/pages/Procesos.jsx
Normal file
@@ -0,0 +1,385 @@
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user