Files
frontend/src/pages/Expedientes.jsx

1800 lines
95 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 { 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 (
<div className="min-h-screen p-4 bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 sm:p-6 lg:p-8">
<div ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
<div className="mx-auto max-w-7xl">
{/* Header mejorado y decorativo */}
<div className={
"mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8 flex 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 p-3 rounded-full shadow-lg bg-white/20 backdrop-blur-sm sm:p-4 animate-bounce-slow">
<svg className="w-8 h-8 text-white sm:h-10 sm:w-10" 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 min-w-0">
<h1 className="flex flex-col gap-2 mb-1 text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl sm:flex-row sm:items-center">
<span>Expedientes</span>
{totalDocuments > 0 && (
<span className="inline-block px-3 py-1 text-xs font-semibold text-white rounded-full shadow-lg bg-white/20 backdrop-blur-sm sm:text-sm animate-fade-in">
{totalDocuments} registros
</span>
)}
</h1>
<p className="text-sm font-medium leading-relaxed text-blue-100 sm:text-lg">Gestiona y descarga los documentos de tus pedimentos</p>
</div>
{/* Efectos decorativos de fondo modernos */}
<div className="absolute pointer-events-none select-none -top-10 -right-10 opacity-20">
<div className="w-32 h-32 rounded-full bg-white/10 blur-xl"></div>
</div>
<div className="absolute pointer-events-none select-none -bottom-6 -left-6 opacity-15">
<div className="w-24 h-24 rounded-full bg-white/10 blur-lg"></div>
</div>
{/* Partículas flotantes */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute w-2 h-2 rounded-full top-1/4 left-1/4 bg-white/30 animate-ping"></div>
<div className="absolute w-1 h-1 rounded-full top-3/4 right-1/3 bg-white/40 animate-pulse"></div>
<div className="absolute w-3 h-3 rounded-full top-1/2 right-1/4 bg-white/20 animate-bounce"></div>
</div>
</div>
{/* Animación personalizada para el icono y contador */}
<style>{`
@keyframes bounce-slow {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-8px) scale(1.05); }
}
.animate-bounce-slow {
animation: bounce-slow 3s infinite;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.8s ease-out;
}
`}</style>
<div className={
"bg-white shadow-2xl rounded-3xl border border-gray-100 overflow-hidden"+
(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 py-4 border-b border-gray-200 sm:px-6 sm:py-6 bg-gradient-to-r from-gray-50 to-blue-50/30">
{/* Filtros avanzados */}
<div className="mb-4 sm:mb-6">
<h3 className="flex items-center mb-3 text-sm font-semibold text-gray-800">
<svg className="w-4 h-4 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z" />
</svg>
Filtros de búsqueda
</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 sm:gap-4">
{/* Search global */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Buscar</label>
<input
type="text"
value={searchFilter}
onChange={e => 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"
/>
</div>
{/* Pedimento_app */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Pedimento</label>
<input
type="text"
value={pedimentoFilter}
onChange={e => 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"
/>
</div>
{/* Expediente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Expediente</label>
<select value={expedienteFilter} onChange={e => setExpedienteFilter(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">
<option value="all">Todos</option>
<option value="true">Con expediente</option>
<option value="false">Sin expediente</option>
</select>
</div>
{/* Contribuyente combobox */}
<div className="relative flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Contribuyente</label>
<input
type="text"
value={contribuyenteInput}
onChange={e => {
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 && (
<div className="absolute left-0 right-0 z-50 overflow-auto bg-white border border-gray-200 shadow-2xl top-16 rounded-xl max-h-40">
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
) : (
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
<button
key={c}
type="button"
className="w-full px-3 py-2 text-sm text-left transition-colors duration-200 hover:bg-blue-50 first:rounded-t-xl last:rounded-b-xl"
onClick={() => {
setContribuyenteFilter(c);
setContribuyenteInput('');
}}
>
{c}
</button>
))
)}
</div>
)}
</div>
{/* CURP Apoderado */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">CURP Apoderado</label>
<input
type="text"
value={curpApoderadoFilter}
onChange={e => 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"
/>
</div>
{/* Fecha de pago */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Fecha de pago</label>
<input type="date" value={fechaPagoFilter} onChange={e => 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" />
</div>
{/* Patente */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Patente</label>
<input
type="text"
value={patenteFilter}
onChange={e => 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"
/>
</div>
{/* Aduana */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Aduana</label>
<input
type="text"
value={aduanaFilter}
onChange={e => 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"
/>
</div>
{/* Tipo de operación */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Tipo de operación</label>
<select
value={tipoOperacionFilter}
onChange={e => setTipoOperacionFilter(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"
>
<option value="">Todos</option>
<option value="1">Importación</option>
<option value="2">Exportación</option>
</select>
</div>
{/* Clave pedimento */}
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-700 mb-1.5">Clave pedimento</label>
<input
type="text"
value={clavePedimentoFilter}
onChange={e => 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"
/>
</div>
</div>
</div>
{/* Área de acciones para documentos seleccionados */}
{selectedDocuments.length > 0 && (
<div className="mb-4 overflow-hidden border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl">
<div className="px-6 py-4 border-b border-blue-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-full">
<svg className="w-5 h-5 text-blue-600" 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>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''} seleccionado{selectedDocuments.length !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-gray-600">Selecciona una acción para continuar</p>
</div>
</div>
<button
onClick={() => {
setSelectedDocuments([]);
setIsSelectAll(false);
}}
className="text-gray-400 transition-colors hover:text-gray-600"
title="Limpiar selección"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<div className="px-6 py-4">
<div className="flex flex-wrap gap-3">
<button
onClick={handleDeleteSelected}
className="inline-flex items-center px-4 py-2 font-medium text-white transition-colors duration-200 bg-red-600 rounded-lg shadow-sm hover:bg-red-700 hover:shadow-md"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Eliminar seleccionados
</button>
</div>
</div>
</div>
)}
<div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div className="flex items-center gap-3">
<span className="inline-flex items-center px-3 py-2 text-xs font-medium text-blue-600 rounded-full bg-blue-50">
<svg className="w-4 h-4 mr-2 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Actualización automática cada 30s
</span>
{loading && (
<span className="inline-flex items-center px-3 py-2 text-xs font-medium text-orange-600 rounded-full bg-orange-50">
<svg className="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Actualizando...
</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
setShowUploadModal(true);
fetchImportadores();
}}
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl 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"
>
<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="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Agregar Expedientes
</button>
<button
onClick={refetch}
disabled={loading}
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Actualizar Ahora
</button>
<button
onClick={() => {handleDownloadSelected()}}
className="inline-flex items-center px-4 py-2.5 border border-transparent text-sm font-medium rounded-xl text-white bg-gradient-to-r from-purple-600 to-purple-700 hover:from-purple-700 hover:to-purple-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200 transform hover:scale-105 shadow-lg"
>
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
{hasActiveFilters ? 'Descargar por Filtro' : 'Descargar Todos'}
</button>
</div>
</div>
{success && (
<div className="p-4 mt-4 border border-green-200 shadow-sm bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl">
<div className="flex">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500" 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"></path>
</svg>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-green-800">{success}</p>
</div>
</div>
</div>
)}
</div>
<div className="overflow-hidden">
{/* Vista de tabla para pantallas grandes */}
<div className="hidden lg:block">
<div className="overflow-x-auto rounded-lg shadow ring-1 ring-black ring-opacity-5">
<table className="min-w-full divide-y divide-gray-300" style={{ minWidth: '1200px' }}>
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<input
type="checkbox"
checked={isSelectAll}
onChange={handleSelectAll}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Pedimento</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Fecha Pago</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Contribuyente</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">CURP Apod.</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Partidas</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">F. Carga</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Tipo Op.</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Clave</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Archivos</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Peso Total</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">Expediente</th>
<th scope="col" className="px-4 py-2 text-xs font-medium tracking-wider text-center text-gray-500 uppercase">Acciones</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={12} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="w-12 h-12 mb-4 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<span className="text-lg font-medium text-gray-500">Cargando expedientes...</span>
</div>
</td>
</tr>
) : error ? (
<tr>
<td colSpan={12} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="p-3 mb-4 bg-red-100 rounded-full">
<svg className="w-8 h-8 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"></path>
</svg>
</div>
<span className="text-lg font-medium text-red-600">Error: {error.message || 'Error al cargar expedientes'}</span>
</div>
</td>
</tr>
) : currentDocuments.length > 0 ? (
currentDocuments.map(ped => (
<tr key={ped.id} className="hover:bg-gray-50">
<td className="px-4 py-3 whitespace-nowrap">
<input
type="checkbox"
checked={selectedDocuments.includes(ped.id)}
onChange={(e) => handleSelectDocument(ped.id, e.target.checked)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-xs font-semibold text-blue-600 transition-colors duration-200 hover:text-blue-800"
>
{ped.pedimento_app}
</Link>
</td>
<td className="px-4 py-3 text-xs text-gray-900 whitespace-nowrap">{ped.fecha_pago}</td>
<td className="max-w-xs px-4 py-3 text-xs text-gray-900 truncate" title={ped.contribuyente}>{ped.contribuyente}</td>
<td className="px-4 py-3 text-xs text-gray-900 whitespace-nowrap">{ped.curp_apoderado}</td>
<td className="px-4 py-3 text-xs text-gray-900 whitespace-nowrap">{ped.numero_partidas}</td>
<td className="px-4 py-3 text-xs text-gray-900 whitespace-nowrap">{ped.created_at ? ped.created_at.slice(0, 10) : ''}</td>
<td className="px-4 py-3 whitespace-nowrap">
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{ped.tipo_operacion === 1
? 'Import.'
: ped.tipo_operacion === 2
? 'Export.'
: ped.tipo_operacion}
</span>
</td>
<td className="px-4 py-3 text-xs text-gray-900 whitespace-nowrap">{ped.clave_pedimento}</td>
<td className="px-4 py-3 text-xs text-gray-900 whitespace-nowrap">{ped.documentos_count || 0}</td>
<td className="min-w-0 px-4 py-3 text-xs text-gray-900 whitespace-nowrap">
<div className="truncate" title={typeof ped.documentos_peso_total === 'number' ? (ped.documentos_peso_total / (1024 * 1024)).toFixed(2) + ' MB' : ped.documentos_peso_total}>
{typeof ped.documentos_peso_total === 'number'
? (ped.documentos_peso_total / (1024 * 1024)).toFixed(2) + ' MB'
: ped.documentos_peso_total || 'N/A'}
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
ped.existe_expediente
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-gray-100 text-gray-600 border border-gray-200'
}`}>
{ped.existe_expediente ? (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</>
) : (
<>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
No
</>
)}
</span>
</td>
<td className="px-4 py-3 text-center whitespace-nowrap">
<button
className="p-2 text-blue-600 transition-colors duration-200 rounded-full hover:text-blue-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={() => handleDownloadTodoElExpediente(ped.id, ped.pedimento_app)}
title="Descargar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={12} className="px-6 py-12 text-center">
<div className="flex flex-col items-center">
<div className="p-4 mb-4 bg-gray-100 rounded-full">
<svg className="w-8 h-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="mb-2 text-lg font-semibold text-gray-900">No hay expedientes</h3>
<p className="text-gray-500">No se encontraron expedientes con los filtros aplicados.</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Vista de tarjetas para pantallas pequeñas y medianas */}
<div className="p-4 space-y-4 lg:hidden">
{loading ? (
<div className="flex flex-col items-center py-12">
<div className="w-12 h-12 mb-4 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<span className="text-lg font-medium text-gray-500">Cargando expedientes...</span>
</div>
) : error ? (
<div className="flex flex-col items-center py-12">
<div className="p-3 mb-4 bg-red-100 rounded-full">
<svg className="w-8 h-8 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"></path>
</svg>
</div>
<span className="text-lg font-medium text-red-600">Error: {error.message || 'Error al cargar expedientes'}</span>
</div>
) : currentDocuments.length > 0 ? (
currentDocuments.map(ped => (
<div key={ped.id} className="relative p-4 transition-all duration-300 bg-white border border-gray-200 shadow-lg rounded-2xl hover:shadow-xl">
{/* Checkbox en la esquina superior derecha */}
<div className="absolute top-4 right-4">
<input
type="checkbox"
checked={selectedDocuments.includes(ped.id)}
onChange={(e) => handleSelectDocument(ped.id, e.target.checked)}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</div>
<div className="flex items-start justify-between pr-8 mb-4">{/* Agregamos pr-8 para dar espacio al checkbox */}
<div className="flex items-center gap-3">
<div className="flex-shrink-0 p-2 bg-blue-100 rounded-xl">
<svg className="w-5 h-5 text-blue-600" 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">
<Link
to={`/expedientes/pedimento/${ped.id}`}
className="text-lg font-semibold text-blue-600 transition-colors duration-200 hover:text-blue-800"
>
{ped.pedimento_app}
</Link>
<p className="text-sm text-gray-500">{ped.fechapago}</p>
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold mt-1 ${
ped.existe_expediente
? 'bg-green-100 text-green-800 border border-green-200'
: 'bg-gray-100 text-gray-600 border border-gray-200'
}`}>
{ped.existe_expediente ? 'Con expediente' : 'Sin expediente'}
</span>
</div>
</div>
</div>
<div className="mb-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">Contribuyente:</span>
<span className="text-sm text-gray-900 text-right max-w-[60%] truncate" title={ped.contribuyente}>
{ped.contribuyente}
</span>
</div>
{ped.curp_apoderado && (
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600">CURP Apoderado:</span>
<span className="text-sm text-gray-900">{ped.curp_apoderado}</span>
</div>
)}
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center justify-between p-2 rounded-lg bg-green-50">
<span className="text-sm font-medium text-green-700">Tipo Operacion</span>
<span className="text-sm font-bold text-green-800">{ped.tipo_operacion}</span>
</div>
<div className="flex items-center justify-between p-2 rounded-lg bg-green-50">
<span className="text-sm font-medium text-green-700">Partidas</span>
<span className="text-sm font-bold text-green-800">{ped.numero_partidas}</span>
</div>
<div className="flex items-center justify-between p-2 rounded-lg bg-blue-50">
<span className="text-sm font-medium text-blue-700">Fecha de Carga</span>
<span className="text-sm font-bold text-blue-800">{ped.created_at ? ped.created_at.slice(0, 10) : ''}</span>
</div>
<div className="flex items-center justify-between p-2 rounded-lg bg-gray-50">
<span className="text-sm font-medium text-gray-700">Clave</span>
<span className="text-sm font-bold text-gray-800">{ped.clave_pedimento}</span>
</div>
<div className="flex items-center justify-between p-2 rounded-lg bg-gray-50">
<span className="text-sm font-medium text-gray-700">No. Archivos</span>
<span className="text-sm font-bold text-gray-800">{ped.documentos_count || 0}</span>
</div>
<div className="flex items-center justify-between p-2 rounded-lg bg-gray-50">
<span className="text-sm font-medium text-gray-700">Peso Total</span>
<span className="text-sm font-bold text-gray-800">
{typeof ped.documentos_peso_total === 'number'
? (ped.documentos_peso_total / (1024 * 1024)).toFixed(2) + ' MB'
: ped.documentos_peso_total || 'N/A'}
</span>
</div>
</div>
</div>
</div>
))
) : (
<div className="p-8 text-center bg-gray-50 rounded-2xl">
<div className="flex items-center justify-center w-16 h-16 p-4 mx-auto mb-4 bg-gray-100 rounded-full">
<svg className="w-8 h-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>
<p className="font-medium text-gray-500">No hay expedientes disponibles</p>
<p className="mt-1 text-sm text-gray-400">Intenta ajustar los filtros de búsqueda</p>
</div>
)}
</div>
{/* Paginación moderna y responsiva */}
{totalDocuments > 0 && (
<div className="flex flex-col items-center justify-between px-4 py-4 border-t border-gray-200 bg-gradient-to-r from-gray-50 to-blue-50/30 sm:px-6 sm:flex-row">
{(() => {
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
const maxPagesToShow = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = startPage + maxPagesToShow - 1;
if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pageNumbers = [];
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(i);
}
return (
<div className="flex flex-col w-full gap-4 sm:flex-row sm:items-center">
<div className="flex items-center gap-3">
<label htmlFor="itemsPerPage" className="text-xs font-semibold text-gray-700">Registros por página:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
className="px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{[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 justify-center flex-1 gap-1 sm:justify-end">
<button
type="button"
onClick={e => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${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 shadow-sm hover:shadow-md'}`}
>
«
</button>
<button
type="button"
onClick={e => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${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 shadow-sm hover:shadow-md'}`}
>
</button>
<div className="items-center hidden gap-1 sm:flex">
{pageNumbers.map(num => (
<button
type="button"
key={num}
onClick={e => handlePageChange(num, e)}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${num === currentPage ? 'bg-blue-600 text-white border-blue-700 shadow-md cursor-default' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
disabled={num === currentPage}
>
{num}
</button>
))}
</div>
<div className="flex items-center px-3 py-2 bg-white border border-gray-200 rounded-lg shadow-sm sm:hidden">
<span className="text-sm font-semibold text-gray-700">
{currentPage} / {totalPages}
</span>
</div>
<button
type="button"
onClick={e => handlePageChange(currentPage + 1, e)}
disabled={currentPage >= totalPages}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
>
</button>
<button
type="button"
onClick={e => handlePageChange(totalPages, e)}
disabled={currentPage >= totalPages}
className={`px-3 py-2 rounded-lg border text-sm font-semibold transition-all duration-200 ${(currentPage >= totalPages) ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' : 'bg-white text-blue-700 border-blue-200 hover:bg-blue-50 hover:text-blue-900 shadow-sm hover:shadow-md'}`}
>
»
</button>
</div>
<div className="text-center sm:text-right">
<span className="px-3 py-2 text-xs text-gray-600 bg-white border border-gray-200 rounded-lg shadow-sm">
Mostrando <span className="font-bold text-blue-600">{((currentPage - 1) * itemsPerPage) + 1}</span> a <span className="font-bold text-blue-600">{Math.min(currentPage * itemsPerPage, totalDocuments)}</span> de <span className="font-bold text-blue-600">{totalDocuments}</span> registros
</span>
</div>
</div>
);
})()}
</div>
)}
</div>
</div>
{/* Modal de subida de expedientes */}
{showUploadModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
<div className="bg-white rounded-2xl shadow-2xl max-w-lg w-full mx-4 transform transition-all duration-300 scale-100 max-h-[90vh] flex flex-col">
{/* Header del modal */}
<div className="flex-shrink-0 px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-3 bg-blue-100 rounded-full">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Subir Expedientes</h3>
<p className="text-sm text-gray-600">Selecciona archivos, carpetas o ZIP</p>
</div>
</div>
<button
onClick={() => {
setShowUploadModal(false);
setSelectedFiles([]);
setUploadProgress(0);
setIsUploading(false);
setValidationErrors([]);
}}
className="text-gray-400 transition-colors duration-200 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Contenido del modal - con scroll */}
<div className="flex-1 min-h-0 px-6 py-4 overflow-y-auto">
{/* Selector de tipo de subida */}
<div className="mb-4">
<label className="block mb-2 text-sm font-medium text-gray-700">
Tipo de subida
</label>
<div className="grid grid-cols-3 gap-2">
<button
onClick={() => setUploadType('folders')}
className={`p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
uploadType === 'folders'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
}`}
>
<svg className="w-5 h-5 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
Carpeta
</button>
<button
onClick={() => setUploadType('zip')}
className={`p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
uploadType === 'zip'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
}`}
>
<svg className="w-5 h-5 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 12l2 2 4-4" />
</svg>
ZIP
</button>
<button
onClick={() => setUploadType('rar')}
className={`p-3 rounded-lg border text-sm font-medium transition-all duration-200 ${
uploadType === 'rar'
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'
}`}
>
<svg className="w-5 h-5 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
RAR
</button>
</div>
</div>
{/* Área de selección de archivos */}
<div className="mb-4">
<label className="block mb-2 text-sm font-medium text-gray-700">
{uploadType === 'folders' && 'Seleccionar carpetas'}
{uploadType === 'zip' && 'Seleccionar archivos ZIP'}
{uploadType === 'rar' && 'Seleccionar archivos RAR'}
</label>
{uploadType === 'folders' ? (
<div className="space-y-3">
<div className="p-6 text-center transition-colors duration-200 border-2 border-gray-300 border-dashed rounded-lg hover:border-gray-400">
<svg className="w-12 h-12 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<p className="mb-3 text-sm text-gray-600">
Selecciona una carpeta. Después puedes hacer clic nuevamente para agregar más carpetas.
</p>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelection}
multiple
webkitdirectory="true"
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-200 bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
>
<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 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Agregar Carpeta
{selectedFiles.length > 0 && (
<span className="px-2 py-1 ml-2 text-xs text-white bg-blue-500 rounded-full">
{[...new Set(selectedFiles.map(file => file.webkitRelativePath.split('/')[0]))].length}
</span>
)}
</button>
</div>
{/* Lista de carpetas seleccionadas - con altura máxima y scroll */}
{selectedFiles.length > 0 && (
<div className="p-4 border border-gray-200 rounded-lg bg-gray-50">
<h4 className="mb-2 text-sm font-medium text-gray-700">Carpetas seleccionadas:</h4>
<div className="space-y-2 overflow-y-auto max-h-40">
{[...new Set(selectedFiles.map(file => file.webkitRelativePath.split('/')[0]))].map((folderName, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-white border rounded">
<div className="flex items-center">
<svg className="w-4 h-4 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<span className="text-sm font-medium">{folderName}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">
{selectedFiles.filter(file => file.webkitRelativePath.startsWith(folderName + '/')).length} archivos
</span>
<button
onClick={() => removeFolderFromSelection(folderName)}
className="p-1 text-red-500 hover:text-red-700"
title="Eliminar carpeta"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
))}
</div>
<button
onClick={() => setSelectedFiles([])}
className="mt-3 text-sm text-red-600 hover:text-red-800"
>
Limpiar todas las carpetas
</button>
</div>
)}
</div>
) : (
<div className="space-y-3">
<div className="p-6 text-center transition-colors duration-200 border-2 border-gray-300 border-dashed rounded-lg hover:border-gray-400">
<svg className="w-12 h-12 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="mb-3 text-sm text-gray-600">
{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.'}
</p>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelection}
accept={uploadType === 'zip' ? '.zip' : '.rar'}
multiple
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-200 bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
>
<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 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Agregar {uploadType === 'zip' ? 'ZIP' : 'RAR'}
{selectedFiles.length > 0 && (
<span className="px-2 py-1 ml-2 text-xs text-white bg-blue-500 rounded-full">
{selectedFiles.length}
</span>
)}
</button>
</div>
{/* Lista de archivos seleccionados */}
{selectedFiles.length > 0 && (
<div className="p-4 border border-gray-200 rounded-lg bg-gray-50">
<h4 className="mb-2 text-sm font-medium text-gray-700">Archivos seleccionados:</h4>
<div className="space-y-2 overflow-y-auto max-h-40">
{selectedFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-white border rounded">
<div className="flex items-center">
<svg className="w-4 h-4 mr-2 text-blue-500" 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>
<span className="text-sm font-medium">{file.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">
{(file.size / 1024 / 1024).toFixed(2)} MB
</span>
<button
onClick={() => removeFileFromSelection(index)}
className="p-1 text-red-500 hover:text-red-700"
title="Eliminar archivo"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
))}
</div>
<button
onClick={() => setSelectedFiles([])}
className="mt-3 text-sm text-red-600 hover:text-red-800"
>
Limpiar todos los archivos
</button>
</div>
)}
</div>
)}
</div>
{/* Selector de contribuyente */}
<div className="mb-4">
<label className="block mb-2 text-sm font-medium text-gray-700">
Contribuyente
</label>
<select
value={selectedContributor}
onChange={(e) => setSelectedContributor(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={loadingContributors}
>
<option value="">Seleccionar contribuyente</option>
{importadores.map((imp) => (
<option key={imp.rfc} value={imp.rfc}>
{imp.rfc}
</option>
))}
</select>
{loadingContributors && (
<p className="mt-1 text-sm text-gray-500">Cargando contribuyentes...</p>
)}
</div>
{/* Errores de validación */}
{validationErrors.length > 0 && (
<div className="p-3 mb-4 border border-red-200 rounded-md bg-red-50">
<div className="flex items-center gap-2 mb-2">
<svg className="w-5 h-5 text-red-500" 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>
<span className="text-sm font-medium text-red-800">Errores de validación:</span>
</div>
<ul className="space-y-1 text-sm text-red-700">
{validationErrors.map((error, index) => (
<li key={index}> {error}</li>
))}
</ul>
</div>
)}
{/* Información de nomenclatura */}
{uploadType === 'folders' && (
<div className="p-3 mb-4 border border-blue-200 rounded-md bg-blue-50">
<div className="flex items-center gap-2 mb-2">
<svg className="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-blue-800">Nomenclatura de carpetas:</span>
</div>
<p className="text-sm text-blue-700">
Las carpetas deben seguir el formato: <strong>[AÑO]-ADUANA-PATENTE-PEDIMENTO</strong>
<br />
AÑO: 2 dígitos (opcional, ej: 24-)
<br />
ADUANA: 2 o 3 dígitos (ej: 01, 001)
<br />
PATENTE: 4 dígitos (ej: 3206)
<br />
PEDIMENTO: 7 dígitos (ej: 1234567)
<br />
<strong>Ejemplos válidos:</strong> <em>24-01-3206-1234567</em>, <em>001-3206-1234567</em>
</p>
</div>
)}
{/* Barras de progreso */}
{isCompressing && (
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Comprimiendo carpetas...</span>
<span className="text-sm text-gray-500">{compressionProgress}%</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full">
<div
className="h-2 transition-all duration-300 bg-green-600 rounded-full"
style={{ width: `${compressionProgress}%` }}
></div>
</div>
</div>
)}
{isUploading && (
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Subiendo...</span>
<span className="text-sm text-gray-500">{uploadProgress}%</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full">
<div
className="h-2 transition-all duration-300 bg-blue-600 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
</div>
)}
</div>
{/* Footer del modal - fijo */}
<div className="flex justify-end flex-shrink-0 gap-3 px-6 py-4 border-t border-gray-200">
<button
onClick={() => {
setShowUploadModal(false);
setSelectedFiles([]);
setUploadProgress(0);
setCompressionProgress(0);
setIsUploading(false);
setIsCompressing(false);
setValidationErrors([]);
}}
disabled={isUploading || isCompressing}
className="px-4 py-2 text-sm font-medium text-gray-700 transition-colors duration-200 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancelar
</button>
<button
onClick={handleUploadFiles}
disabled={isUploading || isCompressing || selectedFiles.length === 0 || !selectedContributor || validationErrors.length > 0}
className="px-4 py-2 text-sm font-medium text-white transition-colors duration-200 bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isCompressing ? 'Comprimiendo...' : isUploading ? 'Subiendo...' : 'Subir expedientes'}
</button>
</div>
</div>
</div>
)}
{/* Modal de confirmación para eliminación */}
{showDeleteModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
<div className="w-full max-w-md mx-4 transition-all duration-300 transform scale-100 bg-white shadow-2xl rounded-2xl">
{/* Header del modal */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center gap-3">
<div className="p-3 bg-red-100 rounded-full">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
Confirmar eliminación
</h3>
<p className="text-sm text-gray-600">Esta acción no se puede deshacer</p>
</div>
</div>
</div>
{/* Contenido del modal */}
<div className="px-6 py-4">
<div className="mb-4">
<p className="mb-3 text-gray-700">
¿Estás seguro de que deseas eliminar{' '}
<span className="font-semibold text-red-600">
{selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''}
</span>
?
</p>
<div className="p-3 border border-red-200 rounded-lg bg-red-50">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-red-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<p className="text-sm font-medium text-red-800">Advertencia importante</p>
<p className="mt-1 text-sm text-red-700">
Los documentos eliminados no podrán ser recuperados. Asegúrate de que realmente deseas proceder con esta acción.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Botones del modal */}
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200">
<button
onClick={() => setShowDeleteModal(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 transition-colors duration-200 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Cancelar
</button>
<button
onClick={confirmDelete}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white transition-colors duration-200 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Eliminar {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''}
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}