Files
frontend/src/pages/Expedientes.jsx

694 lines
42 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, useLayoutEffect, useRef } from 'react';
import { fetchWithAuth, postWithAuth } from '../fetchWithAuth';
// Animación fade-in/slide-up para bloques
const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`;
if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-documents')) {
const style = document.createElement('style');
style.id = 'fadein-slideup-documents';
style.innerHTML = fadeInSlideUp;
document.head.appendChild(style);
}
import { fetchDocuments } from '../api/expedientes.ts';
import { useNotification } from '../context/NotificationContext';
import { usePolling } from '../hooks/usePolling';
import { Link } from 'react-router-dom';
const API_URL = import.meta.env.VITE_EFC_API_URL;
const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => {
try {
const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`);
if (!res.ok) {
alert('No autorizado o error en la descarga');
return;
}
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
if (setSuccess) setSuccess('Descarga exitosa');
} catch (error) {
console.error('Error downloading file:', error);
showMessage('Error al descargar el archivo', 'error');
}
};
export default function Documents() {
const focusKeeperRef = useRef(null);
const [success, setSuccess] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [alertaFilter, setAlertaFilter] = useState('all'); // all, true, false
const [expedienteFilter, setExpedienteFilter] = useState('all'); // all, true, false
const [contribuyenteFilter, setContribuyenteFilter] = useState('');
const [contribuyenteInput, setContribuyenteInput] = useState('');
const [fechaPagoFilter, setFechaPagoFilter] = useState('');
const [pedimentoFilter, setPedimentoFilter] = useState('');
const [searchFilter, setSearchFilter] = useState('');
const [curpApoderadoFilter, setCurpApoderadoFilter] = useState('');
const [patenteFilter, setPatenteFilter] = useState('');
const [aduanaFilter, setAduanaFilter] = useState('');
const [tipoOperacionFilter, setTipoOperacionFilter] = useState('');
const [clavePedimentoFilter, setClavePedimentoFilter] = useState('');
const { showMessage } = useNotification();
// Estado para controlar la animación de entrada
const [showAnimation, setShowAnimation] = useState(false);
const [hasAnimated, setHasAnimated] = useState(false);
useLayoutEffect(() => {
// Forzar un render antes de activar la animación
setShowAnimation(true);
}, []);
useEffect(() => {
if (showAnimation && !hasAnimated) {
const timeout = setTimeout(() => {
setHasAnimated(true);
setShowAnimation(false);
}, 700); // Duración igual a la animación
return () => clearTimeout(timeout);
}
}, [showAnimation, hasAnimated]);
// Fetching usando la función tipada de TypeScript
const fetchPedimentosData = async (page = currentPage, pageSize = itemsPerPage) => {
// Construir objeto de filtros
const filters = {
search: searchFilter || undefined,
pedimento: pedimentoFilter || undefined,
existe_expediente: expedienteFilter === 'all' ? undefined : expedienteFilter,
alerta: alertaFilter === 'all' ? undefined : alertaFilter,
contribuyente: contribuyenteFilter || undefined,
curp_apoderado: curpApoderadoFilter || undefined,
fecha_pago: fechaPagoFilter || undefined,
patente: patenteFilter || undefined,
aduana: aduanaFilter || undefined,
tipo_operacion: tipoOperacionFilter || undefined,
clave_pedimento: clavePedimentoFilter || undefined,
};
return await fetchDocuments(page, pageSize, filters);
};
// Hook de polling que se ejecuta cada 30 segundos
const { data: pedimentos, loading, error, refetch } = usePolling(
() => fetchPedimentosData(currentPage, itemsPerPage),
30000, // 30 segundos
[currentPage, itemsPerPage, searchFilter, pedimentoFilter, expedienteFilter, alertaFilter, contribuyenteFilter, curpApoderadoFilter, fechaPagoFilter, patenteFilter, aduanaFilter, tipoOperacionFilter, clavePedimentoFilter]
);
// Manejo de errores de sesión
useEffect(() => {
if (error && error.message === 'SESSION_EXPIRED') {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
setTimeout(() => {
window.location.href = '/login';
}, 2000);
} else if (error) {
showMessage(error.message, 'error');
}
}, [error, showMessage]);
// Cálculos de paginación usando la estructura tipada
const documentsArray = pedimentos && pedimentos.results ? pedimentos.results : [];
const totalDocuments = pedimentos && typeof pedimentos.count === 'number' ? pedimentos.count : 0;
const totalPages = totalDocuments > 0 ? Math.ceil(totalDocuments / itemsPerPage) : 1;
const currentDocuments = documentsArray;
// Obtener lista única de contribuyentes para el combobox (de la página actual)
const contribuyentes = Array.from(new Set(currentDocuments.map(d => d.contribuyente).filter(Boolean)));
// Refuerza la paginación SPA: nunca recarga la página, solo cambia el estado local
const handlePageChange = (newPage, e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
if (newPage < 1 || newPage > totalPages || newPage === currentPage) return;
setCurrentPage(newPage);
// Quitar el foco del botón activo para evitar salto de scroll
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
// Forzar foco al div invisible para evitar saltos por enfoque automático
useLayoutEffect(() => {
if (focusKeeperRef.current) {
focusKeeperRef.current.focus();
}
}, [currentPage]);
const handleItemsPerPageChange = (newItemsPerPage) => {
setItemsPerPage(newItemsPerPage);
setCurrentPage(1); // Reset a la primera página
};
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
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 ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
<div className="max-w-7xl mx-auto">
{/* Header mejorado y decorativo */}
<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"+
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' } : undefined}>
<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>Expedientes</span>
{totalDocuments > 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">
{totalDocuments} registros
</span>
)}
</h1>
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Gestiona y descarga los documentos de tus pedimentos</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>
</div>
{/* Animación personalizada para el icono y contador */}
<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 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 className={
"bg-white shadow-2xl rounded-3xl border border-gray-100 overflow-hidden"+
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
}
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}>
<div className="px-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-blue-50/30">
{/* Filtros avanzados */}
<div className="mb-4 sm:mb-6">
<h3 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">
<svg className="w-4 h-4 mr-2 text-blue-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 xl:grid-cols-4 gap-3 sm:gap-4">
{/* Search global */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Buscar</label>
<input
type="text"
value={searchFilter}
onChange={e => setSearchFilter(e.target.value)}
placeholder="Buscar pedimento, contribuyente..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Pedimento</label>
<input
type="text"
value={pedimentoFilter}
onChange={e => setPedimentoFilter(e.target.value)}
placeholder="Número de pedimento..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Expediente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Expediente</label>
<select value={expedienteFilter} onChange={e => setExpedienteFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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="all">Todos</option>
<option value="true">Con expediente</option>
<option value="false">Sin expediente</option>
</select>
</div>
{/* Contribuyente combobox */}
<div className="flex flex-col relative">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Contribuyente</label>
<input
type="text"
value={contribuyenteInput}
onChange={e => {
setContribuyenteInput(e.target.value);
setContribuyenteFilter('');
}}
placeholder="Buscar o escribir..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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"
autoComplete="off"
/>
{/* Dropdown de sugerencias */}
{contribuyenteInput && (
<div className="absolute top-16 left-0 right-0 bg-white border border-gray-200 rounded-xl shadow-2xl z-50 max-h-40 overflow-auto">
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
) : (
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
<button
key={c}
type="button"
className="w-full text-left px-3 py-2 hover:bg-blue-50 text-sm transition-colors duration-200 first:rounded-t-xl last:rounded-b-xl"
onClick={() => {
setContribuyenteFilter(c);
setContribuyenteInput('');
}}
>
{c}
</button>
))
)}
</div>
)}
</div>
{/* CURP Apoderado */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">CURP Apoderado</label>
<input
type="text"
value={curpApoderadoFilter}
onChange={e => setCurpApoderadoFilter(e.target.value)}
placeholder="CURP del apoderado..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Fecha de pago */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Fecha de pago</label>
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Patente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Patente</label>
<input
type="text"
value={patenteFilter}
onChange={e => setPatenteFilter(e.target.value)}
placeholder="Patente..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Aduana */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Aduana</label>
<input
type="text"
value={aduanaFilter}
onChange={e => setAduanaFilter(e.target.value)}
placeholder="Aduana..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Tipo de operación */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Tipo de operación</label>
<input
type="text"
value={tipoOperacionFilter}
onChange={e => setTipoOperacionFilter(e.target.value)}
placeholder="ID tipo operación..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
{/* Clave pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Clave pedimento</label>
<input
type="text"
value={clavePedimentoFilter}
onChange={e => setClavePedimentoFilter(e.target.value)}
placeholder="Clave pedimento..."
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 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>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<span className="inline-flex items-center text-xs text-blue-600 bg-blue-50 px-3 py-2 rounded-full font-medium">
<svg className="w-4 h-4 mr-2 animate-pulse" 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>
Actualización automática cada 30s
</span>
{loading && (
<span className="inline-flex items-center text-xs text-orange-600 bg-orange-50 px-3 py-2 rounded-full font-medium">
<svg className="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Actualizando...
</span>
)}
</div>
<button
onClick={refetch}
disabled={loading}
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
<svg className="w-4 h-4 mr-2" 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>
Actualizar Ahora
</button>
</div>
{success && (
<div className="mt-4 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4 shadow-sm">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-500" 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"></path>
</svg>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">{success}</p>
</div>
</div>
</div>
)}
</div>
<div className="overflow-hidden">
{/* Vista de tabla para pantallas grandes */}
<div className="hidden lg:block">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gradient-to-r from-gray-50 to-blue-50">
<tr>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Pedimento</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Fecha de pago</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Contribuyente</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">CURP Apoderado</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Importe total</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Saldo disponible</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Importe pedimento</th>
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Expediente</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100">
{loading ? (
<tr>
<td colSpan={8} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
<span className="text-gray-500 text-lg font-medium">Cargando expedientes...</span>
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={8} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="bg-red-100 rounded-full p-3 mb-4">
<svg className="h-8 w-8 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"></path>
</svg>
</div>
<span className="text-red-600 text-lg font-medium">Error: {error.message || 'Error al cargar expedientes'}</span>
</div>
</td>
</tr>
) : currentDocuments.length > 0 ? (
currentDocuments.map(ped => (
<tr key={ped.id} className="hover:bg-blue-50 transition-all duration-300 group">
<td className="px-4 py-4 whitespace-nowrap">
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-blue-600 hover:text-blue-800 font-semibold transition-colors duration-200 group-hover:underline"
>
{ped.pedimento}
</Link>
</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.fechapago}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700 max-w-xs truncate" title={ped.contribuyente}>{ped.contribuyente}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.curp_apoderado}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_total}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.saldo_disponible}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_pedimento}</td>
<td className="px-4 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
ped.existe_expediente
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-gray-100 text-gray-600 border border-gray-200'
}`}>
{ped.existe_expediente ? (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</>
) : (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
No
</>
)}
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="bg-gray-100 rounded-full p-4 mb-4">
<svg className="h-8 w-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>
<h3 className="text-lg font-semibold text-gray-900 mb-2">No hay expedientes</h3>
<p className="text-gray-500">No se encontraron expedientes con los filtros aplicados.</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="lg:hidden space-y-4 p-4">
{loading ? (
<div className="flex flex-col items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
<span className="text-gray-500 text-lg font-medium">Cargando expedientes...</span>
</div>
) : error ? (
<div className="flex flex-col items-center py-12">
<div className="bg-red-100 rounded-full p-3 mb-4">
<svg className="h-8 w-8 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"></path>
</svg>
</div>
<span className="text-red-600 text-lg font-medium">Error: {error.message || 'Error al cargar expedientes'}</span>
</div>
) : currentDocuments.length > 0 ? (
currentDocuments.map(ped => (
<div key={ped.id} className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 hover:shadow-xl transition-all duration-300 relative">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<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>
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-lg font-semibold text-blue-600 hover:text-blue-800 transition-colors duration-200"
>
{ped.pedimento}
</Link>
<p className="text-sm text-gray-500">{ped.fechapago}</p>
</div>
</div>
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold ${
ped.existe_expediente
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-gray-100 text-gray-600 border border-gray-200'
}`}>
{ped.existe_expediente ? 'Con expediente' : 'Sin expediente'}
</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">Contribuyente:</span>
<span className="text-sm text-gray-900 text-right max-w-[60%] truncate" title={ped.contribuyente}>
{ped.contribuyente}
</span>
</div>
{ped.curp_apoderado && (
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">CURP Apoderado:</span>
<span className="text-sm text-gray-900">{ped.curp_apoderado}</span>
</div>
)}
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center justify-between bg-green-50 rounded-lg p-2">
<span className="text-sm font-medium text-green-700">Importe total:</span>
<span className="text-sm font-bold text-green-800">${ped.importe_total}</span>
</div>
<div className="flex items-center justify-between bg-blue-50 rounded-lg p-2">
<span className="text-sm font-medium text-blue-700">Saldo disponible:</span>
<span className="text-sm font-bold text-blue-800">${ped.saldo_disponible}</span>
</div>
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-2">
<span className="text-sm font-medium text-gray-700">Importe pedimento:</span>
<span className="text-sm font-bold text-gray-800">${ped.importe_pedimento}</span>
</div>
</div>
</div>
</div>
))
) : (
<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 expedientes disponibles</p>
<p className="text-gray-400 text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
</div>
)}
</div>
{/* Paginación moderna y responsiva */}
{totalDocuments > 0 && (
<div className="bg-gradient-to-r from-gray-50 to-blue-50/30 px-4 sm:px-6 py-4 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
{(() => {
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
const maxPagesToShow = 5;
let startPage = Math.max(1, currentPage - 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-4">
<div className="flex items-center gap-3">
<label htmlFor="itemsPerPage" className="text-xs font-semibold text-gray-700">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
className="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-white shadow-sm"
>
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div className="flex items-center justify-center sm:justify-end flex-1 gap-1">
<button
type="button"
onClick={e => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${currentPage === 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 shadow-sm hover:shadow-md'}`}
>
«
</button>
<button
type="button"
onClick={e => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${currentPage === 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 shadow-sm hover:shadow-md'}`}
>
</button>
<div className="hidden sm:flex items-center gap-1">
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${num === currentPage ? 'bg-blue-600 text-white border-blue-700 shadow-md cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
disabled={num === currentPage}
>
{num}
</button>
))}
</div>
<div className="sm:hidden flex items-center px-3 py-2 bg-white border border-gray-200 rounded-lg shadow-sm">
<span className="text-sm font-semibold text-gray-700">
{currentPage} / {totalPages}
</span>
</div>
<button
type="button"
onClick={e => handlePageChange(currentPage + 1, e)}
disabled={currentPage >= totalPages}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= 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 shadow-sm hover:shadow-md'}`}
>
</button>
<button
type="button"
onClick={e => handlePageChange(totalPages, e)}
disabled={currentPage >= totalPages}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= 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 shadow-sm hover:shadow-md'}`}
>
»
</button>
</div>
<div className="text-center sm:text-right">
<span className="text-xs text-gray-600 bg-white px-3 py-2 rounded-lg border border-gray-200 shadow-sm">
Mostrando <span className="font-bold text-blue-600">{((currentPage - 1) * itemsPerPage) + 1}</span> a <span className="font-bold text-blue-600">{Math.min(currentPage * itemsPerPage, totalDocuments)}</span> de <span className="font-bold text-blue-600">{totalDocuments}</span> registros
</span>
</div>
</div>
);
})()}
</div>
)}
</div>
</div>
</div>
</div>
);
}