Se mejoro estetica y estandarizaron estilos
This commit is contained in:
@@ -267,35 +267,35 @@ 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 p-4 sm:p-6 bg-gradient-to-br from-gray-50 to-blue-50">
|
||||
<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-8 relative overflow-hidden rounded-2xl shadow bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 border border-blue-200 p-6 sm:p-8 flex flex-col sm:flex-row items-start sm: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">
|
||||
<div className="flex-1">
|
||||
<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">
|
||||
Documentos
|
||||
<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>
|
||||
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs font-semibold px-3 py-1 rounded-full animate-fade-in">{totalDocuments}</span>
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Descarga los documentos de tus pedimentos.</p>
|
||||
<p className="text-sm sm:text-base lg:text-lg text-blue-100 font-medium">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">
|
||||
<div className="absolute -top-10 -right-10 opacity-20 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" />
|
||||
<stop stopColor="#1e40af" stopOpacity="0.15" />
|
||||
<stop offset="1" stopColor="#1e3a8a" stopOpacity="0.10" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
@@ -324,37 +324,37 @@ export default function Documents() {
|
||||
(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-6 border-b border-gray-200">
|
||||
<div className="overflow-x-auto" id="tabla-documentos">
|
||||
{/* Header de Documentos Relacionados arriba de los filtros */}
|
||||
<div className="px-8 pt-8 pb-2 border-b border-gray-200">
|
||||
<h2 className="text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
|
||||
<div className="px-4 sm:px-6 lg:px-8 pt-6 sm:pt-8 pb-2 border-b border-gray-200">
|
||||
<h2 className="text-xl sm:text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
|
||||
Todos los Documentos
|
||||
</h2>
|
||||
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
|
||||
</div>
|
||||
{/* Filtros de query parameters */}
|
||||
<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">
|
||||
{/* Filtros avanzados */}
|
||||
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
|
||||
<div className="mb-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Pedimento Número */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento Número</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pedimentoNumeroFilter}
|
||||
onChange={e => setPedimentoNumeroFilter(e.target.value)}
|
||||
placeholder="Buscar por número de 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"
|
||||
placeholder="Buscar por número..."
|
||||
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 transition-all"
|
||||
/>
|
||||
</div>
|
||||
{/* Extensión */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Extensión</label>
|
||||
<select
|
||||
value={extensionFilter}
|
||||
onChange={e => setExtensionFilter(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"
|
||||
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 transition-all"
|
||||
>
|
||||
<option value="">Todas</option>
|
||||
<option value="pdf">PDF</option>
|
||||
@@ -372,12 +372,12 @@ export default function Documents() {
|
||||
</select>
|
||||
</div>
|
||||
{/* Tipo de documento */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de documento</label>
|
||||
<select
|
||||
value={documentTypeFilter}
|
||||
onChange={e => setDocumentTypeFilter(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"
|
||||
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 transition-all"
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
<option value="1">Pedimento Partida</option>
|
||||
@@ -389,13 +389,13 @@ export default function Documents() {
|
||||
</select>
|
||||
</div>
|
||||
{/* Fecha de creación */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de creación</label>
|
||||
<input
|
||||
type="date"
|
||||
value={createdAtFilter}
|
||||
onChange={e => setCreatedAtFilter(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"
|
||||
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 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,86 +403,218 @@ export default function Documents() {
|
||||
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
||||
{/* Botones de descarga */}
|
||||
{currentDocuments.length > 0 && (
|
||||
<div className="flex space-x-3 mb-2">
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
||||
<button
|
||||
onClick={handleDownloadAll}
|
||||
disabled={currentDocuments.length === 0}
|
||||
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"
|
||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg 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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
Descargar todos
|
||||
<span className="hidden sm:inline">Descargar todos</span>
|
||||
<span className="sm:hidden">Todos</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownloadSelected}
|
||||
disabled={selectedDocs.length === 0}
|
||||
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-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
|
||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Descargar seleccionados ({selectedDocs.length})
|
||||
<span className="hidden sm:inline">Descargar seleccionados ({selectedDocs.length})</span>
|
||||
<span className="sm:hidden">Seleccionados ({selectedDocs.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: currentDocuments.length > 6 ? 'auto' : 'hidden', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden 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">
|
||||
</div>
|
||||
|
||||
{/* Vista responsiva: tabla para desktop, cards para mobile */}
|
||||
{/* Tabla para pantallas grandes */}
|
||||
<div className="hidden lg:block">
|
||||
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: currentDocuments.length > 6 ? 'auto' : 'hidden', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden 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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
ref={el => { if (el) el.indeterminate = someSelected; }}
|
||||
onChange={handleSelectAll}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Pedimento</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Archivo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tamaño</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Extensión</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
||||
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={10} 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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : error ? (
|
||||
<tr>
|
||||
<td colSpan={10} 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-red-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : currentDocuments.length > 0 ? (
|
||||
<>
|
||||
{currentDocuments.map(doc => (
|
||||
<tr key={doc.id} className="transition-all duration-200 hover:bg-blue-50 hover:shadow-lg">
|
||||
<td className="px-2 py-2 text-center align-middle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocs.includes(doc.id)}
|
||||
onChange={() => handleSelectOne(doc.id)}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{doc.pedimento_numero}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{doc.archivo ? doc.archivo.split('/').pop() : ''}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{
|
||||
(() => {
|
||||
switch (String(doc.document_type)) {
|
||||
case '1': return 'Pedimento Partida';
|
||||
case '2': return 'Pedimento Completo';
|
||||
case '3': return 'Pedimento Remesas';
|
||||
case '4': return 'Pedimento Acuse';
|
||||
case '5': return 'Pedimento EDocument';
|
||||
case '6': return 'Estado Pedimento';
|
||||
default: return doc.document_type || '';
|
||||
}
|
||||
})()
|
||||
}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.size}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.extension}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
|
||||
<button
|
||||
className="inline-flex items-center px-3 py-1 border border-transparent text-xs font-semibold 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"
|
||||
title="Descargar"
|
||||
onClick={async () => {
|
||||
await downloadFile(
|
||||
doc.id,
|
||||
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
|
||||
() => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
},
|
||||
null,
|
||||
showMessage
|
||||
);
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
</button>
|
||||
</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-2 py-4" />
|
||||
<td className="px-6 py-4 whitespace-nowrap" colSpan={5}> </td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} 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">
|
||||
<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 documentos</h3>
|
||||
<p className="text-gray-500">Aún no tienes documentos registrados.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards para pantallas pequeñas */}
|
||||
<div className="lg:hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<span className="text-gray-500 text-lg">Cargando documentos...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-12 w-12 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-red-600 text-lg">Error: {error.message || 'Error al cargar documentos'}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : currentDocuments.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{/* Selección múltiple en mobile */}
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
ref={el => { if (el) el.indeterminate = someSelected; }}
|
||||
onChange={handleSelectAll}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Pedimento</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Archivo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tamaño</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Extensión</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
||||
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={10} 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>
|
||||
<span className="text-sm font-medium text-gray-700">Seleccionar todos</span>
|
||||
</label>
|
||||
<span className="text-sm text-gray-500">{selectedDocs.length} seleccionados</span>
|
||||
</div>
|
||||
{currentDocuments.map(doc => (
|
||||
<div key={doc.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start space-x-3 mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocs.includes(doc.id)}
|
||||
onChange={() => handleSelectOne(doc.id)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
Pedimento: {doc.pedimento_numero}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-1 break-all">
|
||||
{doc.archivo ? doc.archivo.split('/').pop() : ''}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : error ? (
|
||||
<tr>
|
||||
<td colSpan={10} 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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : currentDocuments.length > 0 ? (
|
||||
<>
|
||||
{currentDocuments.map(doc => (
|
||||
<tr key={doc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
|
||||
<td className="px-2 py-2 text-center align-middle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocs.includes(doc.id)}
|
||||
onChange={() => handleSelectOne(doc.id)}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{doc.pedimento_numero}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{doc.archivo ? doc.archivo.split('/').pop() : ''}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-xs mb-4">
|
||||
<div>
|
||||
<span className="font-medium text-gray-500">Tipo:</span>
|
||||
<p className="text-gray-900 mt-1">{
|
||||
(() => {
|
||||
switch (String(doc.document_type)) {
|
||||
case '1': return 'Pedimento Partida';
|
||||
@@ -494,150 +626,142 @@ export default function Documents() {
|
||||
default: return doc.document_type || '';
|
||||
}
|
||||
})()
|
||||
}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.size}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.extension}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
|
||||
<button
|
||||
className="inline-flex items-center px-3 py-1 border border-transparent text-xs font-semibold 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"
|
||||
title="Descargar"
|
||||
onClick={async () => {
|
||||
await downloadFile(
|
||||
doc.id,
|
||||
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
|
||||
() => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
},
|
||||
null,
|
||||
showMessage
|
||||
);
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
</button>
|
||||
</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-2 py-4" />
|
||||
<td className="px-6 py-4 whitespace-nowrap" colSpan={5}> </td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} 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">
|
||||
<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>
|
||||
}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<span className="font-medium text-gray-500">Tamaño:</span>
|
||||
<p className="text-gray-900 mt-1">{doc.size}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-500">Extensión:</span>
|
||||
<p className="text-gray-900 mt-1 uppercase">{doc.extension}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-gray-100">
|
||||
<button
|
||||
className="w-full inline-flex items-center justify-center px-3 py-2 border border-transparent text-xs font-semibold rounded-lg 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 shadow"
|
||||
onClick={async () => {
|
||||
await downloadFile(
|
||||
doc.id,
|
||||
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
|
||||
() => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
},
|
||||
null,
|
||||
showMessage
|
||||
);
|
||||
}}
|
||||
>
|
||||
<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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
Descargar Documento
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<div className="mx-auto h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center 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 documentos</h3>
|
||||
<p className="text-gray-500 text-center">Aún no tienes documentos registrados.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de actualizar eliminado por solicitud */}
|
||||
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden">
|
||||
|
||||
{/* Paginación con botones numerados y elipsis */}
|
||||
{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">
|
||||
{(() => {
|
||||
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-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 => 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"
|
||||
>
|
||||
{[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 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 => 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"
|
||||
>
|
||||
{[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">
|
||||
<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'}`}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
{(() => {
|
||||
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 pageNumbers.map(num => (
|
||||
<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'}`}
|
||||
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>
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
‹
|
||||
</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>
|
||||
))}
|
||||
<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'}`}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
»
|
||||
</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>
|
||||
);
|
||||
})()}
|
||||
));
|
||||
})()}
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => handlePageChange(currentPage + 1, e)}
|
||||
disabled={currentPage >= Math.ceil(totalDocuments / itemsPerPage)}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= Math.ceil(totalDocuments / itemsPerPage)) ? '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 => handlePageChange(Math.ceil(totalDocuments / itemsPerPage), e)}
|
||||
disabled={currentPage >= Math.ceil(totalDocuments / itemsPerPage)}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= Math.ceil(totalDocuments / itemsPerPage)) ? '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">{currentPage}</span> de <span className="font-bold">{Math.ceil(totalDocuments / itemsPerPage)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user