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 (
{/* Header mejorado y decorativo */}

Documentos {totalDocuments}

Descarga los documentos de tus pedimentos.

{/* Efecto decorativo de fondo */}
{/* Animación personalizada para el icono y contador */}
{/* Header de Documentos Relacionados arriba de los filtros */}

Todos los Documentos

{/* Filtros de query parameters */}
{/* Filtros avanzados */}
{/* Pedimento Número */}
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" />
{/* Extensión */}
{/* Tipo de documento */}
{/* Fecha de creación */}
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" />
{/* Botón de actualizar eliminado por solicitud */} setShowSuccessModal(false)} message={success || 'Descarga exitosa'} /> {/* Botones de descarga */} {currentDocuments.length > 0 && (
)}
{/* Vista responsiva: tabla para desktop, cards para mobile */} {/* Tabla para pantallas grandes */}
6 ? 'auto' : 'hidden', position: 'relative' }}> {/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */} {loading ? ( ) : error ? ( ) : currentDocuments.length > 0 ? ( <> {currentDocuments.map(doc => ( ))} {/* Rellenar con filas vacías si hay menos de 8 */} {currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => ( ))} ) : ( )}
{ 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' }} /> Pedimento Archivo Tipo Tamaño Extensión Acciones
Cargando documentos...
Error: {error.message || 'Error al cargar documentos'}
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' }} /> {doc.pedimento_numero} {doc.archivo ? doc.archivo.split('/').pop() : ''} { (() => { 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 || ''; } })() } {doc.size} {doc.extension}
 

No hay documentos

Aún no tienes documentos registrados.

{/* Cards para pantallas pequeñas */}
{loading ? (
Cargando documentos...
) : error ? (
Error: {error.message || 'Error al cargar documentos'}
) : currentDocuments.length > 0 ? (
{/* Selección múltiple en mobile */}
{selectedDocs.length} seleccionados
{currentDocuments.map(doc => (
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" />

Pedimento: {doc.pedimento_numero}

{doc.archivo ? doc.archivo.split('/').pop() : ''}

Tipo:

{ (() => { 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 || ''; } })() }

Tamaño:

{doc.size}

Extensión:

{doc.extension}

))}
) : (

No hay documentos

Aún no tienes documentos registrados.

)}
{/* Botón de actualizar eliminado por solicitud */} setShowSuccessModal(false)} message={success || 'Descarga exitosa'} /> {/* Paginación con botones numerados y elipsis */} {totalDocuments > 0 && (
{(() => { 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 => ( )); })()} Página {currentPage} de {Math.ceil(totalDocuments / itemsPerPage)}
)}
); }