diff --git a/package-lock.json b/package-lock.json index 88757b3..57f0e3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@tanstack/react-query": "^5.62.7", "chart.js": "^4.5.0", "highlight.js": "^11.11.1", + "jszip": "^3.10.1", "react": "^19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", @@ -1706,6 +1707,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2420,6 +2427,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2429,6 +2442,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-binary-path": { "version": "2.1.0", "dev": true, @@ -2488,6 +2507,12 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -2531,6 +2556,27 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2703,6 +2749,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/path-parse": { "version": "1.0.7", "license": "MIT" @@ -2919,6 +2971,12 @@ "node": ">=0.10.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/react": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", @@ -3029,6 +3087,21 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "dev": true, @@ -3069,6 +3142,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -3082,11 +3161,26 @@ "semver": "bin/semver.js" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -3727,7 +3821,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/vite": { diff --git a/package.json b/package.json index ba3d754..784ce1c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.62.7", "chart.js": "^4.5.0", "highlight.js": "^11.11.1", + "jszip": "^3.10.1", "react": "^19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", diff --git a/src/pages/Expedientes.jsx b/src/pages/Expedientes.jsx index dd6b03c..401be41 100644 --- a/src/pages/Expedientes.jsx +++ b/src/pages/Expedientes.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useLayoutEffect, useRef } from 'react'; -import { fetchWithAuth, postWithAuth } from '../fetchWithAuth'; +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')) { @@ -42,6 +43,7 @@ const downloadFile = async (id, filename = 'archivo', setSuccess, setError, show 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); @@ -66,6 +68,20 @@ export default function Documents() { // 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); @@ -255,6 +271,243 @@ export default function Documents() { } }; + // 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); + + showMessage( + `${result.uploaded_count || fileCount} archivo(s) subido(s) exitosamente`, + 'success' + ); + + // 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) { @@ -550,6 +803,18 @@ export default function Documents() { )}
+
+ {/* 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 && (
diff --git a/src/pages/PedimentoDetail.jsx b/src/pages/PedimentoDetail.jsx index d56a5b3..c47fc85 100644 --- a/src/pages/PedimentoDetail.jsx +++ b/src/pages/PedimentoDetail.jsx @@ -16,7 +16,7 @@ import xml from 'highlight.js/lib/languages/xml'; import 'highlight.js/styles/github.css'; hljs.registerLanguage('xml', xml); import { fetchPedimentoDocuments } from '../api/pedimentoDocuments'; -import { fetchWithAuth, postWithAuth, putWithAuth } from '../fetchWithAuth'; +import { fetchWithAuth, postWithAuth, putWithAuth, postFormDataWithAuth } from '../fetchWithAuth'; import { fetchTasks } from '../api/procesos.ts'; import { fetchPedimentoCoves, downloadCove, downloadAcuseCove } from '../api/coves'; import { fetchPedimentoEdocuments, downloadEdocument, downloadAcuseEdocument } from '../api/edocuments'; @@ -131,6 +131,11 @@ export default function PedimentoDetail() { const [selectedDocuments, setSelectedDocuments] = useState([]); const [isSelectAllDocs, setIsSelectAllDocs] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showUploadModal, setShowUploadModal] = useState(false); + + // Estados para subir documentos + const [selectedFiles, setSelectedFiles] = useState([]); + const [uploadingDocuments, setUploadingDocuments] = useState(false); const [dashboardSummary, setDashboardSummary] = useState(null); const [showFilters, setShowFilters] = useState(false); @@ -673,6 +678,56 @@ export default function PedimentoDetail() { } }; + // Funciones para subir documentos + const handleFileSelect = (event) => { + const files = Array.from(event.target.files); + setSelectedFiles(files); + }; + + const handleUploadDocuments = async () => { + if (selectedFiles.length === 0) { + showMessage('Por favor selecciona al menos un archivo', 'warning'); + return; + } + + setUploadingDocuments(true); + + try { + const formData = new FormData(); + + // Agregar el ID del pedimento + formData.append('pedimento_id', id); + + // Agregar archivos al FormData + selectedFiles.forEach((file) => { + formData.append('files', file); + }); + + showMessage(`Subiendo ${selectedFiles.length} archivo(s)...`, 'info'); + + const result = await postFormDataWithAuth(`${API_URL}/record/documents/bulk-upload/`, formData); + + showMessage( + `${result.uploaded_count || selectedFiles.length} archivo(s) subido(s) exitosamente`, + 'success' + ); + + // Limpiar archivos seleccionados y cerrar modal + setSelectedFiles([]); + setShowUploadModal(false); + + // Forzar recarga de documentos + const currentPage = page; + setPage(0); + setTimeout(() => setPage(currentPage), 100); + } catch (error) { + console.error('Error durante la subida:', error); + showMessage(`Error durante la subida: ${error.message}`, 'error'); + } finally { + setUploadingDocuments(false); + } + }; + // Efecto para actualizar isSelectAllDocs cuando cambia la selección useEffect(() => { if (documents.length > 0) { @@ -2192,45 +2247,58 @@ export default function PedimentoDetail() {
- {documents.length > 0 && ( -
- - - -
- )} +
+ + + {documents.length > 0 && ( + <> + + + + + )} +
{/* Filtros expandibles */} @@ -4950,6 +5018,109 @@ export default function PedimentoDetail() { )} + {/* Modal para subir documentos */} + {showUploadModal && ( +
+
+ {/* Header del modal */} +
+
+
+ + + +
+
+

+ Subir Documentos +

+

Selecciona los archivos a subir

+
+
+
+ + {/* Contenido del modal */} +
+
+ + + + {selectedFiles.length > 0 && ( +
+

+ Archivos seleccionados ({selectedFiles.length}): +

+
+ {selectedFiles.map((file, index) => ( +
+ {file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB) +
+ ))} +
+
+ )} + +
+
+ + + +
+

Información

+

+ Los archivos se subirán al pedimento actual. Se aceptan múltiples formatos de archivo. +

+
+
+
+
+
+ + {/* Botones del modal */} +
+ + +
+
+
+ )} + {/* Modal de confirmación para eliminación */} {showDeleteModal && (