Se mejoro estetica y estandarizaron estilos

This commit is contained in:
2025-08-05 10:30:25 -06:00
parent c3d800ba48
commit aa515c1d01
12 changed files with 3530 additions and 1611 deletions

View File

@@ -160,326 +160,349 @@ export default function Documents() {
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
return (
<div className="p-6 bg-gray-50">
<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-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"+
"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-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">
<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>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
Expedientes
<span className="inline-block bg-blue-200 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded-full ml-2 animate-fade-in">{totalDocuments}</span>
<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-lg text-blue-700/80 font-medium">Gestiona y descarga los documentos de tus pedimentos.</p>
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Gestiona y descarga los documentos de tus pedimentos</p>
</div>
{/* Efecto decorativo de fondo */}
<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>
{/* 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); }
50% { transform: translateY(-8px); }
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
animation: bounce-slow 3s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
animation: fade-in 0.8s ease-out;
}
`}</style>
<div className={
"bg-white shadow-lg rounded-xl border border-gray-200"+
"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-6 py-6 border-b border-gray-200">
<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 flex flex-wrap gap-4 items-end">
{/* Search global */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Buscar</label>
<input
type="text"
value={searchFilter}
onChange={e => setSearchFilter(e.target.value)}
placeholder="Buscar pedimento, contribuyente, agente aduanal..."
className="w-44 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>
{/* Pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
<input
type="text"
value={pedimentoFilter}
onChange={e => setPedimentoFilter(e.target.value)}
placeholder="Buscar pedimento..."
className="w-36 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>
{/* Alerta */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Alerta</label>
<select value={alertaFilter} onChange={e => setAlertaFilter(e.target.value)}
className="w-36 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="all">Todos</option>
<option value="true"></option>
<option value="false">No</option>
</select>
</div>
{/* Expediente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Expediente</label>
<select value={expedienteFilter} onChange={e => setExpedienteFilter(e.target.value)}
className="w-36 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="all">Todos</option>
<option value="true"></option>
<option value="false">No</option>
</select>
</div>
{/* Contribuyente combobox */}
<div className="flex flex-col relative">
<label className="text-xs font-semibold text-gray-700 mb-1">Contribuyente</label>
<input
type="text"
value={contribuyenteInput}
onChange={e => {
setContribuyenteInput(e.target.value);
setContribuyenteFilter('');
}}
placeholder="Buscar o escribir..."
className="w-44 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"
autoComplete="off"
/>
{/* Dropdown de sugerencias */}
{contribuyenteInput && (
<div className="absolute top-14 left-0 w-44 bg-white border border-gray-200 rounded-lg shadow-lg z-10 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"
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">CURP Apoderado</label>
<input
type="text"
value={curpApoderadoFilter}
onChange={e => setCurpApoderadoFilter(e.target.value)}
placeholder="CURP del apoderado..."
className="w-44 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>
{/* Fecha de pago */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de pago</label>
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
className="w-44 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>
{/* Patente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Patente</label>
<input
type="text"
value={patenteFilter}
onChange={e => setPatenteFilter(e.target.value)}
placeholder="Patente..."
className="w-36 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>
{/* Aduana */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Aduana</label>
<input
type="text"
value={aduanaFilter}
onChange={e => setAduanaFilter(e.target.value)}
placeholder="Aduana..."
className="w-36 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>
{/* Tipo de operación */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de operación</label>
<input
type="text"
value={tipoOperacionFilter}
onChange={e => setTipoOperacionFilter(e.target.value)}
placeholder="ID tipo operación..."
className="w-36 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>
{/* Clave pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1">Clave pedimento</label>
<input
type="text"
value={clavePedimentoFilter}
onChange={e => setClavePedimentoFilter(e.target.value)}
placeholder="Clave pedimento..."
className="w-36 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 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 items-center justify-between">
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
🔄 Actualización automática cada 30 segundos
</span>
<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}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md 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={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-green-50 border border-green-200 rounded-md p-4">
<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">
<svg className="h-5 w-5 text-green-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
<p className="text-green-800">{success}</p>
<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">
<div className="overflow-x-auto" id="tabla-documentos">
<div style={{ minHeight: 'calc(7 * 56px)', maxHeight: 'calc(7 * 56px)', overflowY: currentDocuments.length > 8 ? 'auto' : 'hidden', position: 'relative' }}>
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
<thead className="bg-gray-50 sticky top-0 z-20 shadow">
{/* 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-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">Fecha de pago</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Contribuyente</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Alerta</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">CURP Apoderado</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Importe total</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Saldo disponible</th>
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Importe pedimento</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expediente</th>
<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-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
<tbody className="bg-white divide-y divide-gray-100">
{loading ? (
<tr>
<td colSpan={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-gray-500 text-lg">Cargando documentos...</span>
<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={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<span className="text-danger-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
<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 hover:scale-[1.02] hover:shadow-md">
<td className="px-6 py-4 whitespace-nowrap">
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200"
>
{ped.pedimento}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.fechapago}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.contribuyente}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
ped.alerta
? 'bg-red-100 text-red-800'
: 'bg-green-100 text-green-800'
}`}>
{ped.alerta ? 'Sí' : 'No'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.curp_apoderado}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_total}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.saldo_disponible}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_pedimento}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
ped.existe_expediente
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{ped.existe_expediente ? 'Sí' : 'No'}
</span>
</td>
</tr>
))}
{/* Rellenar con filas vacías si hay menos de 8 */}
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
<tr key={`empty-${idx}`} className="">
<td className="px-6 py-4 whitespace-nowrap" colSpan={9}>&nbsp;</td>
</tr>
))}
</>
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={9} style={{ height: 'calc(8 * 56px)', padding: 0 }}>
<div className="flex flex-col items-center justify-center h-full w-full absolute left-0 top-0" style={{ minHeight: 'calc(8 * 56px)', background: 'rgba(255,255,255,0.7)', zIndex: 10 }}>
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<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-medium text-gray-900 mb-2">No hay pedimentos</h3>
<p className="text-gray-500">Aún no tienes pedimentos registrados.</p>
<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>
@@ -488,9 +511,97 @@ export default function Documents() {
</table>
</div>
</div>
{/* Paginación con botones numerados y elipsis */}
{/* 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-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
<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;
@@ -505,26 +616,26 @@ export default function Documents() {
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>
<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 px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
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 gap-1 flex-wrap">
<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-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${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'}`}
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>
@@ -532,26 +643,33 @@ export default function Documents() {
type="button"
onClick={e => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${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'}`}
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>
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === currentPage ? '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 === currentPage}
>
{num}
</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-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(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'}`}
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>
@@ -559,11 +677,15 @@ export default function Documents() {
type="button"
onClick={e => handlePageChange(totalPages, e)}
disabled={currentPage >= totalPages}
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(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'}`}
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>
<span className="ml-3 text-xs text-gray-500">Página <span className="font-bold">{currentPage}</span> de <span className="font-bold">{totalPages}</span></span>
</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>
);