Files
frontend/src/pages/Documents.jsx
2025-08-14 16:35:09 -06:00

751 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 SuccessModal from '../components/SuccessModal.jsx';
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 { fetchPedimentoDocuments } from '../api/documentos.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;
// Descarga individual
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');
}
};
// Descarga masiva (bulk)
const downloadBulkZip = async (ids, showMessage, setSuccess, nombreZip = 'documentos') => {
if (!ids.length) {
showMessage('Selecciona al menos un documento.', 'error');
return;
}
try {
const res = await postWithAuth(`${API_URL}/record/documents/bulk-download/`, {
document_ids: ids,
pedimento_nombre: nombreZip
});
if (!res.ok) {
showMessage('No autorizado o error en la descarga masiva', 'error');
return;
}
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${nombreZip || 'documentos'}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
if (setSuccess) setSuccess('Descarga(s) completada(s)');
} catch (error) {
console.error('Error in bulk download:', error);
showMessage('Error en la descarga masiva', 'error');
}
};
export default function Documents() {
const focusKeeperRef = useRef(null);
const [success, setSuccess] = useState('');
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [extensionFilter, setExtensionFilter] = useState('');
const [documentTypeFilter, setDocumentTypeFilter] = useState('');
const [createdAtFilter, setCreatedAtFilter] = useState('');
const [pedimentoNumeroFilter, setPedimentoNumeroFilter] = 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]);
// Estado local para los datos, loading y error
const [docsData, setDocsData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Fetch de datos solo al cargar la página o cuando cambian los filtros/paginación
useEffect(() => {
let isMounted = true;
const fetchDocsData = async () => {
setLoading(true);
setError(null);
try {
const data = await fetchPedimentoDocuments(currentPage, itemsPerPage, {
pedimento_numero: pedimentoNumeroFilter,
extension: extensionFilter,
document_type: documentTypeFilter,
created_at: createdAtFilter,
});
if (isMounted) setDocsData(data);
} catch (err) {
if (isMounted) setError(err);
} finally {
if (isMounted) setLoading(false);
}
};
fetchDocsData();
return () => { isMounted = false; };
}, [currentPage, itemsPerPage, pedimentoNumeroFilter, extensionFilter, documentTypeFilter, createdAtFilter]);
// Refetch manual (si se quiere usar en el futuro)
const refetch = () => {
setCurrentPage(1); // Esto forzará el useEffect a recargar
};
// 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 = docsData && docsData.results ? docsData.results : [];
const totalDocuments = docsData && typeof docsData.count === 'number' ? docsData.count : 0;
const totalPages = totalDocuments > 0 ? Math.ceil(totalDocuments / itemsPerPage) : 1;
const currentDocuments = documentsArray;
// Selección de documentos
const [selectedDocs, setSelectedDocs] = useState([]);
// allSelected: todos los docs de la página actual están seleccionados
const allSelected = currentDocuments.length > 0 && selectedDocs.length === currentDocuments.length;
// someSelected: hay al menos uno seleccionado pero no todos
const someSelected = selectedDocs.length > 0 && selectedDocs.length < currentDocuments.length;
// Handlers para selección
const handleSelectOne = (id) => {
setSelectedDocs(prev => prev.includes(id) ? prev.filter(d => d !== id) : [...prev, id]);
};
const handleSelectAll = () => {
if (allSelected) {
setSelectedDocs([]);
} else {
setSelectedDocs(currentDocuments.map(doc => doc.id));
}
};
// Descargar seleccionados (bulk) con prompt para nombre del zip
const handleDownloadSelected = async () => {
const ids = currentDocuments.filter(doc => selectedDocs.includes(doc.id)).map(doc => doc.id);
if (ids.length === 1) {
// Si solo hay uno, descarga individual
const doc = currentDocuments.find(doc => doc.id === ids[0]);
await downloadFile(doc.id, doc.archivo ? doc.archivo.split('/').pop() : 'archivo', () => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
}, null, showMessage);
} else if (ids.length > 1) {
let nombreZip = window.prompt('¿Qué nombre quieres para el archivo zip?', 'documentos_seleccionados');
if (!nombreZip) nombreZip = 'documentos_seleccionados';
await downloadBulkZip(ids, showMessage, () => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
}, nombreZip);
}
};
// Descargar todos los de la página (bulk) con prompt para nombre del zip
const handleDownloadAll = async () => {
const ids = currentDocuments.map(doc => doc.id);
if (ids.length === 1) {
const doc = currentDocuments[0];
await downloadFile(doc.id, doc.archivo ? doc.archivo.split('/').pop() : 'archivo', () => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
}, null, showMessage);
} else if (ids.length > 1) {
let nombreZip = window.prompt('¿Qué nombre quieres para el archivo zip?', 'documentos_pagina');
if (!nombreZip) nombreZip = 'documentos_pagina';
await downloadBulkZip(ids, showMessage, () => {
setSuccess('Descarga exitosa');
setShowSuccessModal(true);
}, nombreZip);
}
};
// Limpiar selección al cambiar de página o documentos
useEffect(() => {
setSelectedDocs([]);
}, [currentPage, itemsPerPage, pedimentoNumeroFilter, extensionFilter, documentTypeFilter, createdAtFilter, docsData]);
// 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 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-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-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">
<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-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-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-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="#1e40af" stopOpacity="0.15" />
<stop offset="1" stopColor="#1e3a8a" stopOpacity="0.10" />
</linearGradient>
</defs>
</svg>
</div>
</div>
{/* Animación personalizada para el icono y contador */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.animate-bounce-slow {
animation: bounce-slow 2.2s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.animate-fade-in {
animation: fade-in 0.7s ease;
}
`}</style>
<div className={
"bg-white shadow-lg rounded-xl border border-gray-200"+
(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-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-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-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200">
{/* Filtros avanzados */}
<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">
<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..."
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">
<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 transition-all"
>
<option value="">Todas</option>
<option value="pdf">PDF</option>
<option value="xml">XML</option>
</select>
</div>
{/* Tipo de documento */}
<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 transition-all"
>
<option value="">Todos</option>
<option value="1">Pedimento Partida</option>
<option value="2">Pedimento Completo</option>
<option value="3">Pedimento Remesas</option>
<option value="4">Pedimento Acuse</option>
<option value="5">Pedimento EDocument</option>
<option value="6">Estado Pedimento</option>
<option value="7">Acuse Cove</option>
</select>
</div>
{/* Fecha de creación */}
<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 transition-all"
/>
</div>
</div>
{/* Botón de actualizar eliminado por solicitud */}
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
{/* Botones de descarga */}
{currentDocuments.length > 0 && (
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<button
onClick={handleDownloadAll}
disabled={currentDocuments.length === 0}
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>
<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 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>
<span className="hidden sm:inline">Descargar seleccionados ({selectedDocs.length})</span>
<span className="sm:hidden">Seleccionados ({selectedDocs.length})</span>
</button>
</div>
)}
</div>
</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';
case '7': return 'Acuse Cove';
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}>&nbsp;</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-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<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>
</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';
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 || '';
}
})()
}</p>
</div>
<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'} />
{/* 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">
<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"
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 >= 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>
);
}