import React, { useEffect, useState, useLayoutEffect, useRef } from 'react'; import { fetchWithAuth, postWithAuth, postFormDataWithAuth } from '../fetchWithAuth'; import JSZip from 'jszip'; // Animación fade-in/slide-up para bloques const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`; if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-documents')) { const style = document.createElement('style'); style.id = 'fadein-slideup-documents'; style.innerHTML = fadeInSlideUp; document.head.appendChild(style); } import { fetchDocuments } from '../api/expedientes.ts'; import { useNotification } from '../context/NotificationContext'; import { usePolling } from '../hooks/usePolling'; import { Link } from 'react-router-dom'; const API_URL = import.meta.env.VITE_EFC_API_URL; const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => { try { console.log('Descargar: ',id); const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`); if (!res.ok) { //alert('No autorizado o error en la descarga'); showMessage('No autorizado o error en la descarga.', 'error'); return false; } 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'); return false; } return true; }; export default function Documents() { const focusKeeperRef = useRef(null); const fileInputRef = useRef(null); const [success, setSuccess] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); const [alertaFilter, setAlertaFilter] = useState('all'); // all, true, false const [expedienteFilter, setExpedienteFilter] = useState('all'); // all, true, false const [contribuyenteFilter, setContribuyenteFilter] = useState(''); const [contribuyenteInput, setContribuyenteInput] = useState(''); const [fechaPagoFilter, setFechaPagoFilter] = useState(''); const [pedimentoFilter, setPedimentoFilter] = useState(''); const [searchFilter, setSearchFilter] = useState(''); const [curpApoderadoFilter, setCurpApoderadoFilter] = useState(''); const [patenteFilter, setPatenteFilter] = useState(''); const [aduanaFilter, setAduanaFilter] = useState(''); const [tipoOperacionFilter, setTipoOperacionFilter] = useState(''); const [clavePedimentoFilter, setClavePedimentoFilter] = useState(''); const { showMessage } = useNotification(); // Estados para selección múltiple const [selectedDocuments, setSelectedDocuments] = useState([]); const [isSelectAll, setIsSelectAll] = useState(false); // Estado para modal de confirmación de eliminación const [showDeleteModal, setShowDeleteModal] = useState(false); // Estados para subir expedientes const [showUploadModal, setShowUploadModal] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const [uploadType, setUploadType] = useState('folders'); // 'folders', 'zip', 'rar' const [uploadingFiles, setUploadingFiles] = useState(false); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [isCompressing, setIsCompressing] = useState(false); const [compressionProgress, setCompressionProgress] = useState(0); const [validationErrors, setValidationErrors] = useState([]); const [importadores, setImportadores] = useState([]); const [selectedContributor, setSelectedContributor] = useState(''); const [loadingContributors, setLoadingContributors] = useState(false); // Estado para controlar la animación de entrada const [showAnimation, setShowAnimation] = useState(false); const [hasAnimated, setHasAnimated] = useState(false); useLayoutEffect(() => { // Forzar un render antes de activar la animación setShowAnimation(true); }, []); useEffect(() => { if (showAnimation && !hasAnimated) { const timeout = setTimeout(() => { setHasAnimated(true); setShowAnimation(false); }, 700); // Duración igual a la animación return () => clearTimeout(timeout); } }, [showAnimation, hasAnimated]); // Fetching usando la función tipada de TypeScript const fetchPedimentosData = async (page = currentPage, pageSize = itemsPerPage) => { // Construir objeto de filtros const filters = { search: searchFilter || undefined, pedimento_app: pedimentoFilter || undefined, existe_expediente: expedienteFilter === 'all' ? undefined : expedienteFilter, contribuyente: contribuyenteFilter || undefined, curp_apoderado: curpApoderadoFilter || undefined, fecha_pago: fechaPagoFilter || undefined, patente: patenteFilter || undefined, aduana: aduanaFilter || undefined, tipo_operacion: tipoOperacionFilter || undefined, clave_pedimento: clavePedimentoFilter || undefined, }; return await fetchDocuments(page, pageSize, filters); }; // Hook de polling que se ejecuta cada 30 segundos const { data: pedimentos, loading, error, refetch } = usePolling( () => fetchPedimentosData(currentPage, itemsPerPage), 30000, // 30 segundos [currentPage, itemsPerPage, searchFilter, pedimentoFilter, expedienteFilter, alertaFilter, contribuyenteFilter, curpApoderadoFilter, fechaPagoFilter, patenteFilter, aduanaFilter, tipoOperacionFilter, clavePedimentoFilter] ); // Manejo de errores de sesión useEffect(() => { if (error && error.message === 'SESSION_EXPIRED') { localStorage.removeItem('access'); localStorage.removeItem('refresh'); showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); setTimeout(() => { window.location.href = '/login'; }, 2000); } else if (error) { showMessage(error.message, 'error'); } }, [error, showMessage]); // Cálculos de paginación usando la estructura tipada const documentsArray = pedimentos && pedimentos.results ? pedimentos.results : []; const totalDocuments = pedimentos && typeof pedimentos.count === 'number' ? pedimentos.count : 0; const totalPages = totalDocuments > 0 ? Math.ceil(totalDocuments / itemsPerPage) : 1; const currentDocuments = documentsArray; // Obtener lista única de contribuyentes para el combobox (de la página actual) const contribuyentes = Array.from(new Set(currentDocuments.map(d => d.contribuyente).filter(Boolean))); // Refuerza la paginación SPA: nunca recarga la página, solo cambia el estado local const handlePageChange = (newPage, e) => { if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (e && typeof e.stopPropagation === 'function') e.stopPropagation(); if (newPage < 1 || newPage > totalPages || newPage === currentPage) return; setCurrentPage(newPage); // Quitar el foco del botón activo para evitar salto de scroll if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }; // Forzar foco al div invisible para evitar saltos por enfoque automático useLayoutEffect(() => { if (focusKeeperRef.current) { focusKeeperRef.current.focus(); } }, [currentPage]); const handleItemsPerPageChange = (newItemsPerPage) => { setItemsPerPage(newItemsPerPage); setCurrentPage(1); // Reset a la primera página }; // Funciones para manejo de selección múltiple const handleSelectDocument = (documentId, isSelected) => { if (isSelected) { setSelectedDocuments(prev => [...prev, documentId]); } else { setSelectedDocuments(prev => prev.filter(id => id !== documentId)); } }; const handleSelectAll = () => { if (isSelectAll) { setSelectedDocuments([]); setIsSelectAll(false); } else { const allDocumentIds = currentDocuments.map(doc => doc.id); setSelectedDocuments(allDocumentIds); setIsSelectAll(true); } }; // // Función para descargar documentos seleccionados // const handleDownloadSelected = async () => { // if (selectedDocuments.length === 0) { // showMessage('No hay documentos seleccionados para descargar', 'warning'); // return; // } // try { // showMessage(`Iniciando descarga de ${selectedDocuments.length} documento(s)...`, 'info'); // const resultados = []; // for (const docId of selectedDocuments) { // const document = currentDocuments.find(doc => doc.id === docId); // if (document) { // // const exito =await downloadFile(docId, `expediente_${document.pedimento_app}`, null, null, showMessage); // const exito =await downloadExpediente(docId, `expediente_${document.pedimento_app}`, null, showMessage); // resultados.push(exito); // // Pequeña pausa entre descargas para no sobrecargar // await new Promise(resolve => setTimeout(resolve, 500)); // } // } // const todosExitosos = resultados.every(Boolean); // if(todosExitosos){ // showMessage(`Descarga completada de ${selectedDocuments.length} documento(s)`, 'success'); // }else{ // showMessage('Algunas descargas fallaron. Revisa los archivos seleccionados.', 'warning'); // } // setSelectedDocuments([]); // setIsSelectAll(false); // } catch (error) { // showMessage('Error durante la descarga masiva', 'error'); // } // // showMessage('Error durante la descarga masiva', 'error'); // }; // Función para descargar documentos seleccionados const handleDownloadSelected = async () => { if (selectedDocuments.length === 0) { showMessage('No hay documentos seleccionados para descargar', 'warning'); return; } try { showMessage(`Iniciando descarga de ${selectedDocuments.length} documento(s) selecciobado(s)...`, 'info'); const res = await postWithAuth(`${API_URL}/record/documents/multi-pedimento-zip/`, { pedimento_ids: selectedDocuments }); if (!res.ok){ showMessage('Algunas descargas fallaron. Revisa los archivos seleccionados.', 'warning'); }else { // Leer el nombre que eligió el backend const zipFileName = res.headers.get('X-Zip-Filename') || 'expedientes.zip'; const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = zipFileName; a.click(); a.remove(); window.URL.revokeObjectURL(url); showMessage(`Descarga completada de ${selectedDocuments.length} documento(s)`, 'success'); setSelectedDocuments([]); setIsSelectAll(false); } } catch (error) { showMessage('Error durante la descarga masiva', 'error'); } // showMessage('Error durante la descarga masiva', 'error'); }; // Función para descargar todo el expediente const handleDownloadTodoElExpediente = async (pedimentoId, pedimentoName) => { try { showMessage(`Iniciando descarga de ${selectedDocuments.length} documento(s)...`, 'info'); const resultados = []; const document = currentDocuments.find(doc => doc.id === pedimentoId); if (document) { const exito =await downloadExpediente(pedimentoId, `expediente_${pedimentoName}`, null, showMessage); resultados.push(exito); // Pequeña pausa entre descargas para no sobrecargar await new Promise(resolve => setTimeout(resolve, 500)); } const todosExitosos = resultados.every(Boolean); if(todosExitosos){ showMessage(`Descarga completada de ${selectedDocuments.length} documento(s)`, 'success'); }else{ // showMessage('Algunas descargas fallaron. Revisa los archivos seleccionados.', 'warning'); } setSelectedDocuments([]); setIsSelectAll(false); } catch (error) { showMessage('Error durante la descarga masiva', 'error'); } }; const downloadExpediente = async (pedimentoId, pedimentoName, setSuccess, showMessage) => { try { const res = await postWithAuth(`${API_URL}/record/documents/expediente-zip/`, { pedimento_id: pedimentoId, }); if (!res.ok) { // alert('No autorizado o error en la descarga'); // showMessage('No autorizado o error en la descarga.', 'error'); const err = await res.json(); showMessage(err.error || 'Error al generar ZIP', 'error'); return false; } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${pedimentoName}.zip`; // 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'); return false; } return true; }; // Función para eliminar documentos seleccionados const handleDeleteSelected = async () => { if (selectedDocuments.length === 0) { showMessage('No hay documentos seleccionados para eliminar', 'warning'); return; } // Mostrar modal de confirmación setShowDeleteModal(true); }; // Función para confirmar la eliminación const confirmDelete = async () => { setShowDeleteModal(false); try { showMessage(`Eliminando ${selectedDocuments.length} documento(s)...`, 'info'); // Enviar todos los IDs seleccionados a un endpoint específico const response = await fetchWithAuth(`${API_URL}/customs/pedimentos/bulk-delete/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ids: selectedDocuments }) }); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(errorData?.detail || errorData?.message || `Error ${response.status}: ${response.statusText}`); } const result = await response.json(); showMessage( `${result.deleted_count || selectedDocuments.length} documento(s) eliminado(s) exitosamente`, 'success' ); setSelectedDocuments([]); setIsSelectAll(false); // Refrescar la lista refetch(); } catch (error) { console.error('Error durante la eliminación masiva:', error); showMessage(`Error durante la eliminación: ${error.message}`, 'error'); } }; // Funciones para subir expedientes const fetchImportadores = async () => { if (importadores.length > 0) return; // Ya están cargados setLoadingContributors(true); try { const response = await fetchWithAuth(`${API_URL}/customs/importadores/`); if (response.ok) { const data = await response.json(); // La API devuelve directamente el array, no un objeto con results setImportadores(Array.isArray(data) ? data : []); } else { throw new Error('Error al cargar importadores'); } } catch (error) { console.error('Error loading importadores:', error); showMessage('Error al cargar la lista de importadores', 'error'); } finally { setLoadingContributors(false); } }; const handleFileSelect = (event) => { const files = Array.from(event.target.files); setSelectedFiles(files); }; const validateFolderNomenclature = (files) => { // Patrón flexible: [AÑO(2 dígitos opcional)]-[ADUANA(2-3 dígitos)]-[PATENTE(4 dígitos)]-[PEDIMENTO(7 dígitos)] // El año puede estar presente o ausente, si está presente debe ser 2 dígitos // La aduana puede ser 2 o 3 dígitos const pattern = /^(\d{2}-)?(\d{2,3})-(\d{4})-(\d{7})$/; const invalidFolders = []; for (const file of files) { const pathParts = file.webkitRelativePath.split('/'); if (pathParts.length > 1) { const folderName = pathParts[0]; if (!pattern.test(folderName)) { invalidFolders.push(folderName); } } } return invalidFolders; }; // Función para comprimir carpetas en ZIP const compressFoldersToZip = async (files) => { const zip = new JSZip(); const folderGroups = {}; // Agrupar archivos por carpeta files.forEach(file => { const pathParts = file.webkitRelativePath.split('/'); const folderName = pathParts[0]; if (!folderGroups[folderName]) { folderGroups[folderName] = []; } folderGroups[folderName].push(file); }); const folderNames = Object.keys(folderGroups); const compressedFiles = []; setIsCompressing(true); setCompressionProgress(0); try { // Comprimir cada carpeta individualmente for (let i = 0; i < folderNames.length; i++) { const folderName = folderNames[i]; const folderFiles = folderGroups[folderName]; const folderZip = new JSZip(); // Agregar archivos al ZIP de la carpeta folderFiles.forEach(file => { // Mantener la estructura de subcarpetas dentro de la carpeta principal const relativePath = file.webkitRelativePath.substring(folderName.length + 1); folderZip.file(relativePath, file); }); // Generar el ZIP de la carpeta const zipBlob = await folderZip.generateAsync( { type: "blob" }, (metadata) => { // Actualizar progreso de compresión const folderProgress = metadata.percent; const totalProgress = ((i * 100) + folderProgress) / folderNames.length; setCompressionProgress(Math.round(totalProgress)); } ); // Crear un archivo File a partir del blob const zipFile = new File([zipBlob], `${folderName}.zip`, { type: 'application/zip' }); compressedFiles.push(zipFile); } return compressedFiles; } finally { setIsCompressing(false); setCompressionProgress(0); } }; // Función para manejar la selección de archivos const handleFileSelection = (event) => { const files = Array.from(event.target.files); if (uploadType === 'folders') { // Para carpetas, agregar a los archivos existentes (acumular) setSelectedFiles(prevFiles => [...prevFiles, ...files]); setValidationErrors([]); // Validar nomenclatura de todas las carpetas después de agregar setTimeout(() => { setSelectedFiles(currentFiles => { const invalidFolders = validateFolderNomenclature(currentFiles); if (invalidFolders.length > 0) { setValidationErrors([ `Las siguientes carpetas no siguen la nomenclatura correcta ([AÑO]-ADUANA-PATENTE-PEDIMENTO): ${invalidFolders.join(', ')}` ]); } return currentFiles; }); }, 100); } else { // Para ZIP/RAR, también permitir acumular múltiples archivos setSelectedFiles(prevFiles => [...prevFiles, ...files]); setValidationErrors([]); } // Limpiar el input para permitir seleccionar la misma carpeta nuevamente event.target.value = ''; }; // Función para eliminar una carpeta específica const removeFolderFromSelection = (folderNameToRemove) => { setSelectedFiles(prevFiles => prevFiles.filter(file => !file.webkitRelativePath.startsWith(folderNameToRemove + '/')) ); setValidationErrors([]); }; // Función para eliminar un archivo específico (ZIP/RAR) const removeFileFromSelection = (fileIndex) => { setSelectedFiles(prevFiles => prevFiles.filter((_, index) => index !== fileIndex) ); setValidationErrors([]); }; const handleUploadFiles = async () => { if (!selectedContributor) { showMessage('Por favor selecciona un importador', 'warning'); return; } if (selectedFiles.length === 0) { showMessage('Por favor selecciona al menos un archivo', 'warning'); return; } // Validar nomenclatura si es tipo carpeta if (uploadType === 'folders') { const invalidFolders = validateFolderNomenclature(selectedFiles); if (invalidFolders.length > 0) { setValidationErrors([ `Las siguientes carpetas no siguen la nomenclatura correcta ([AÑO]-ADUANA-PATENTE-PEDIMENTO): ${invalidFolders.join(', ')}` ]); return; } } setIsUploading(true); setUploadProgress(0); try { const formData = new FormData(); formData.append('contribuyente', selectedContributor); let filesToUpload = selectedFiles; // Comprimir carpetas automáticamente if (uploadType === 'folders') { showMessage('Comprimiendo carpetas...', 'info'); filesToUpload = await compressFoldersToZip(selectedFiles); formData.append('tipo', 'zip'); // Cambiar tipo a zip después de comprimir } else { formData.append('tipo', uploadType); } // Agregar archivos al FormData if (uploadType === 'folders') { // Para carpetas comprimidas, agregar como múltiples archivos ZIP filesToUpload.forEach((file, index) => { formData.append(`archivos`, file); }); } else { // Para ZIP/RAR múltiples, agregar cada archivo filesToUpload.forEach((file, index) => { formData.append(`archivos`, file); }); } const fileCount = uploadType === 'folders' ? filesToUpload.length : selectedFiles.length; showMessage(`Subiendo ${fileCount} archivo(s)...`, 'info'); const uploadEndpoint = `${API_URL}/customs/pedimentos/bulk-create/`; const result = await postFormDataWithAuth(uploadEndpoint, formData); if(!result.ok){ let errorMsg = 'Error al intentar cargar los archivos'; try { const errorData = await result.json(); errorMsg = errorData.error || errorMsg; } catch { // Si no es JSON válido, usar texto plano const text = await result.text(); errorMsg = text || errorMsg; } showMessage(errorMsg, 'error'); }else{ showMessage( `${result.uploaded_count || fileCount} archivo(s) subido(s) exitosamente`, 'success' ); } // showMessage( // `${result.uploaded_count || fileCount} archivo(s) subido(s) exitosamente`, // 'success' // ); // console.log(result); // Limpiar archivos seleccionados y cerrar modal setSelectedFiles([]); setSelectedContributor(''); setUploadProgress(0); setCompressionProgress(0); setValidationErrors([]); setShowUploadModal(false); // Refrescar la lista refetch(); } catch (error) { console.error('Error durante la subida:', error); showMessage(`Error durante la subida: ${error.message}`, 'error'); } finally { setIsUploading(false); } }; // Actualizar isSelectAll cuando cambia la selección useEffect(() => { if (currentDocuments.length > 0) { const allSelected = currentDocuments.every(doc => selectedDocuments.includes(doc.id)); setIsSelectAll(allSelected && selectedDocuments.length > 0); } }, [selectedDocuments, currentDocuments]); // Limpiar selección cuando cambia de página useEffect(() => { setSelectedDocuments([]); setIsSelectAll(false); }, [currentPage]); // función que detecta si hay filtros activos const hasActiveFilters = [ searchFilter, pedimentoFilter, expedienteFilter !== 'all', contribuyenteFilter, curpApoderadoFilter, fechaPagoFilter, patenteFilter, aduanaFilter, tipoOperacionFilter, clavePedimentoFilter, ].some(Boolean); // El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla return (
{/* Header mejorado y decorativo */}

Expedientes {totalDocuments > 0 && ( {totalDocuments} registros )}

Gestiona y descarga los documentos de tus pedimentos

{/* Efectos decorativos de fondo modernos */}
{/* Partículas flotantes */}
{/* Animación personalizada para el icono y contador */}
{/* Filtros avanzados */}

Filtros de búsqueda

{/* Search global */}
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" />
{/* Pedimento_app */}
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" />
{/* Expediente */}
{/* Contribuyente combobox */}
{ setContribuyenteInput(e.target.value); setContribuyenteFilter(e.target.value); }} onBlur={e => { setContribuyenteFilter(e.target.value); }} 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 && (
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
Sin coincidencias
) : ( contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => ( )) )}
)}
{/* CURP Apoderado */}
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" />
{/* Fecha de pago */}
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" />
{/* Patente */}
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" />
{/* Aduana */}
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" />
{/* Tipo de operación */}
{/* Clave pedimento */}
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" />
{/* Área de acciones para documentos seleccionados */} {selectedDocuments.length > 0 && (

{selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} seleccionado{selectedDocuments.length !== 1 ? 's' : ''}

Selecciona una acción para continuar

)}
Actualización automática cada 30s {loading && ( Actualizando... )}
{success && (

{success}

)}
{/* Vista de tabla para pantallas grandes */}
{loading ? ( ) : error ? ( ) : currentDocuments.length > 0 ? ( currentDocuments.map(ped => ( )) ) : ( )}
Pedimento Fecha Pago Contribuyente CURP Apod. Partidas F. Carga Tipo Op. Clave Archivos Peso Total Expediente Acciones
Cargando expedientes...
Error: {error.message || 'Error al cargar expedientes'}
handleSelectDocument(ped.id, e.target.checked)} className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" /> {ped.pedimento_app} {ped.fecha_pago} {ped.contribuyente} {ped.curp_apoderado} {ped.numero_partidas} {ped.created_at ? ped.created_at.slice(0, 10) : ''} {ped.tipo_operacion === 1 ? 'Import.' : ped.tipo_operacion === 2 ? 'Export.' : ped.tipo_operacion} {ped.clave_pedimento} {ped.documentos_count || 0}
{typeof ped.documentos_peso_total === 'number' ? (ped.documentos_peso_total / (1024 * 1024)).toFixed(2) + ' MB' : ped.documentos_peso_total || 'N/A'}
{ped.existe_expediente ? ( <> Sí ) : ( <> No )}

No hay expedientes

No se encontraron expedientes con los filtros aplicados.

{/* Vista de tarjetas para pantallas pequeñas y medianas */}
{loading ? (
Cargando expedientes...
) : error ? (
Error: {error.message || 'Error al cargar expedientes'}
) : currentDocuments.length > 0 ? ( currentDocuments.map(ped => (
{/* Checkbox en la esquina superior derecha */}
handleSelectDocument(ped.id, e.target.checked)} className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
{/* Agregamos pr-8 para dar espacio al checkbox */}
{ped.pedimento_app}

{ped.fechapago}

{ped.existe_expediente ? 'Con expediente' : 'Sin expediente'}
Contribuyente: {ped.contribuyente}
{ped.curp_apoderado && (
CURP Apoderado: {ped.curp_apoderado}
)}
Tipo Operacion {ped.tipo_operacion}
Partidas {ped.numero_partidas}
Fecha de Carga {ped.created_at ? ped.created_at.slice(0, 10) : ''}
Clave {ped.clave_pedimento}
No. Archivos {ped.documentos_count || 0}
Peso Total {typeof ped.documentos_peso_total === 'number' ? (ped.documentos_peso_total / (1024 * 1024)).toFixed(2) + ' MB' : ped.documentos_peso_total || 'N/A'}
)) ) : (

No hay expedientes disponibles

Intenta ajustar los filtros de búsqueda

)}
{/* Paginación moderna y responsiva */} {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 => ( ))}
{currentPage} / {totalPages}
Mostrando {((currentPage - 1) * itemsPerPage) + 1} a {Math.min(currentPage * itemsPerPage, totalDocuments)} de {totalDocuments} registros
); })()}
)}
{/* Modal de subida de expedientes */} {showUploadModal && (
{/* Header del modal */}

Subir Expedientes

Selecciona archivos, carpetas o ZIP

{/* Contenido del modal - con scroll */}
{/* Selector de tipo de subida */}
{/* Área de selección de archivos */}
{uploadType === 'folders' ? (

Selecciona una carpeta. Después puedes hacer clic nuevamente para agregar más carpetas.

{/* Lista de carpetas seleccionadas - con altura máxima y scroll */} {selectedFiles.length > 0 && (

Carpetas seleccionadas:

{[...new Set(selectedFiles.map(file => file.webkitRelativePath.split('/')[0]))].map((folderName, index) => (
{folderName}
{selectedFiles.filter(file => file.webkitRelativePath.startsWith(folderName + '/')).length} archivos
))}
)}
) : (

{uploadType === 'zip' && 'Selecciona archivos ZIP. Puedes hacer clic nuevamente para agregar más archivos.'} {uploadType === 'rar' && 'Selecciona archivos RAR. Puedes hacer clic nuevamente para agregar más archivos.'}

{/* Lista de archivos seleccionados */} {selectedFiles.length > 0 && (

Archivos seleccionados:

{selectedFiles.map((file, index) => (
{file.name}
{(file.size / 1024 / 1024).toFixed(2)} MB
))}
)}
)}
{/* Selector de contribuyente */}
{loadingContributors && (

Cargando contribuyentes...

)}
{/* Errores de validación */} {validationErrors.length > 0 && (
Errores de validación:
    {validationErrors.map((error, index) => (
  • • {error}
  • ))}
)} {/* Información de nomenclatura */} {uploadType === 'folders' && (
Nomenclatura de carpetas:

Las carpetas deben seguir el formato: [AÑO]-ADUANA-PATENTE-PEDIMENTO
• AÑO: 2 dígitos (opcional, ej: 24-)
• ADUANA: 2 o 3 dígitos (ej: 01, 001)
• PATENTE: 4 dígitos (ej: 3206)
• PEDIMENTO: 7 dígitos (ej: 1234567)
Ejemplos válidos: 24-01-3206-1234567, 001-3206-1234567

)} {/* Barras de progreso */} {isCompressing && (
Comprimiendo carpetas... {compressionProgress}%
)} {isUploading && (
Subiendo... {uploadProgress}%
)}
{/* Footer del modal - fijo */}
)} {/* Modal de confirmación para eliminación */} {showDeleteModal && (
{/* Header del modal */}

Confirmar eliminación

Esta acción no se puede deshacer

{/* Contenido del modal */}

¿Estás seguro de que deseas eliminar{' '} {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} ?

Advertencia importante

Los documentos eliminados no podrán ser recuperados. Asegúrate de que realmente deseas proceder con esta acción.

{/* Botones del modal */}
)}
); }