Files
frontend/src/pages/PedimentoDetail.jsx

7222 lines
361 KiB
JavaScript

import React, { useEffect, useState, useRef } from 'react';
import { FileUp } from 'lucide-react';
// 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-pedimento')) {
const style = document.createElement('style');
style.id = 'fadein-slideup-pedimento';
style.innerHTML = fadeInSlideUp;
document.head.appendChild(style);
}
import hljs from 'highlight.js/lib/core';
import xml from 'highlight.js/lib/languages/xml';
import 'highlight.js/styles/github.css';
hljs.registerLanguage('xml', xml);
import { fetchPedimentoDocuments } from '../api/pedimentoDocuments';
import { fetchPedimentoCompleto} from '../api/pedimentoCompleto';
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';
import { getTaskStatusLabel, getTaskStatusColor, isTaskActionable, isTaskFinal } from '../api/taskStatus';
import { useParams, Link } from 'react-router-dom';
import { useNotification } from '../context/NotificationContext';
import { downloadFile, downloadBulkZip } from '../utils/downloadUtils';
const API_URL = import.meta.env.VITE_EFC_API_URL;
const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
const MICROSERVICE_URL_2 = import.meta.env.VITE_EFC_MICROSERVICE_URL_2;
// Funciones auxiliares para estados y servicios (copiadas de procesos.js)
const getEstadoLabel = (estado) => {
const estados = {
1: 'Pendiente',
2: 'En Proceso',
3: 'Completado',
4: 'Error',
5: 'Cancelado'
};
return estados[estado] || `Estado ${estado}`;
};
const getEstadoColor = (estado) => {
const colores = {
1: 'bg-yellow-100 text-yellow-800',
2: 'bg-blue-100 text-blue-800',
3: 'bg-green-100 text-green-800',
4: 'bg-red-100 text-red-800',
5: 'bg-gray-100 text-gray-800'
};
return colores[estado] || 'bg-gray-100 text-gray-800';
};
const getServicioLabel = (servicio) => {
const servicios = {
1: 'Estado de Pedimento',
3: 'Pedimento Completo',
4: 'Partidas',
5: 'Remesa',
6: 'Acuse ',
7: 'EDocuments',
8: 'Acuse de Cove',
9: 'Cove'
};
return servicios[servicio] || `Servicio ${servicio}`;
};
const getServicioColor = (servicio) => {
const colores = {
1: 'bg-purple-100 text-purple-800',
2: 'bg-indigo-100 text-indigo-800',
3: 'bg-blue-100 text-blue-800',
4: 'bg-cyan-100 text-cyan-800',
5: 'bg-teal-100 text-teal-800',
6: 'bg-green-100 text-green-800',
7: 'bg-yellow-100 text-yellow-800',
8: 'bg-orange-100 text-orange-800',
9: 'bg-pink-100 text-pink-800'
};
return colores[servicio] || 'bg-gray-100 text-gray-800';
};
// Función para formatear XML (pretty print)
function formatXml(xml) {
const PADDING = ' ';
const reg = /(>)(<)(\/*)/g;
let formatted = '';
let pad = 0;
xml = xml.replace(reg, '$1\r\n$2$3');
xml.split(/\r?\n/).forEach((node) => {
let indent = 0;
if (node.match(/.+<\/\w[^>]*>$/)) {
indent = 0;
} else if (node.match(/^<\/\w/)) {
if (pad !== 0) pad -= 1;
} else if (node.match(/^<\w[^>]*[^\/]>/)) {
indent = 1;
}
formatted += PADDING.repeat(pad) + node + '\r\n';
pad += indent;
});
return formatted.trim();
}
export default function PedimentoDetail() {
// Estados principales
// const [activeTab, setActiveTab] = useState('documentos');
const [activeTab, setActiveTab] = useState('pedimento');
const [pedimento, setPedimento] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { id } = useParams();
const { showMessage } = useNotification();
// Estados para documentos
const [documents, setDocuments] = useState([]);
const [docsCount, setDocsCount] = useState(0);
const [docsLoading, setDocsLoading] = useState(true);
const [docsError, setDocsError] = useState('');
const [docsNext, setDocsNext] = useState(null);
const [docsPrev, setDocsPrev] = useState(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selected, setSelected] = useState([]);
const [downloading, setDownloading] = useState(false);
const [downloadingAll, setDownloadingAll] = useState(false);
// Estados para selección múltiple con checkboxes
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);
const [showFiltersPedimento, setShowFiltersPedimento] = useState(false);
// Filtros simplificados de documentos (nuevo diseño)
const [filters, setFilters] = useState({
name: '',
document_type: '',
extension: '',
date: '',
created_at__gte: '',
created_at__lte: '',
fuente: '',
pedimento_numero: ''
});
// Filtros simplificados de pedimento (nuevo diseño)
const [pedimentoFilters, setPedimentoFilters] = useState({
name: '',
document_type: '',
extension: '',
date: '',
created_at__gte: '',
created_at__lte: '',
fuente: '',
pedimento_numero: ''
});
// Estados para filtros (legacy - para compatibilidad)
const [documentTypeFilter, setDocumentTypeFilter] = useState('');
const [fileNameFilter, setFileNameFilter] = useState('');
const [extensionFilter, setExtensionFilter] = useState('');
const [dateFilter, setDateFilter] = useState('');
const [orderBy, setOrderBy] = useState('');
const [orderDir, setOrderDir] = useState('asc');
// Estados para COVEs
const [coves, setCoves] = useState([]);
const [covesCount, setCovesCount] = useState(0);
const [covesLoading, setCovesLoading] = useState(false);
const [covesError, setCovesError] = useState('');
const [covesPage, setCovesPage] = useState(1);
const [covesPageSize, setCovesPageSize] = useState(10);
const [covesFilters, setCovesFilters] = useState({
numero_cove: '',
cove_descargado: '',
acuse_cove_descargado: '',
created_at__gte: '',
created_at__lte: ''
});
// Estados para EDocs
const [edocs, setEdocs] = useState([]);
const [edocsCount, setEdocsCount] = useState(0);
const [edocsLoading, setEdocsLoading] = useState(false);
const [edocsError, setEdocsError] = useState('');
const [edocsPage, setEdocsPage] = useState(1);
const [edocsPageSize, setEdocsPageSize] = useState(10);
const [edocsFilters, setEdocsFilters] = useState({
numero_edocument: '',
clave: '',
descripcion: '',
edocument_descargado: '',
acuse_descargado: '',
created_at__gte: '',
created_at__lte: ''
});
// Estados para Pedimento T2025-10-152
const [peddocuments, setPedDocuments] = useState([]);
const [peddocsCount, setPedDocsCount] = useState(0);
const [pedimentoNext, setPedimentoNext] = useState(null);
const [pedimentoPrev, setPedimentoPrev] = useState(null);
const [pedimentoLoading, setPedimentoLoading] = useState(false);
const [pedimentoError, setPedimentoError] = useState('');
const [pedimentoPage, setPedimentoPage] = useState(1);
const [pedimentoPageSize, setPedimentoPageSize] = useState(10);
const [downloadingAllPedimento, setDownloadingAllPedimento] = useState(false);
// Agrega estos estados para selección de documentos de pedimento
const [selectedPedimentoDocuments, setSelectedPedimentoDocuments] = useState([]);
const [isSelectAllPedimentoDocs, setIsSelectAllPedimentoDocs] = useState(false);
// Efecto para actualizar isSelectAllPedimentoDocs cuando cambia la selección
useEffect(() => {
if (peddocuments.length > 0) {
const allSelected = peddocuments.every(doc => selectedPedimentoDocuments.includes(doc.id));
setIsSelectAllPedimentoDocs(allSelected && selectedPedimentoDocuments.length > 0);
}
}, [selectedPedimentoDocuments, peddocuments]);
// Efecto para limpiar selección cuando cambia de página
useEffect(() => {
setSelectedPedimentoDocuments([]);
setIsSelectAllPedimentoDocs(false);
}, [pedimentoPage]);
// Funciones para manejo de selección múltiple de documentos de pedimento
const handleSelectPedimentoDocument = (documentId) => {
const isSelected = selectedPedimentoDocuments.includes(documentId);
if (isSelected) {
setSelectedPedimentoDocuments(prev => prev.filter(id => id !== documentId));
} else {
setSelectedPedimentoDocuments(prev => [...prev, documentId]);
}
};
const handleSelectAllPedimentoDocuments = () => {
if (isSelectAllPedimentoDocs) {
setSelectedPedimentoDocuments([]);
setIsSelectAllPedimentoDocs(false);
} else {
const allDocumentIds = peddocuments.map(doc => doc.id);
setSelectedPedimentoDocuments(allDocumentIds);
setIsSelectAllPedimentoDocs(true);
}
};
// Función para eliminar documentos seleccionados de pedimento
const handleDeleteSelectedPedimentoDocuments = async () => {
if (selectedPedimentoDocuments.length === 0) {
showMessage('No hay documentos seleccionados para eliminar', 'warning');
return;
}
try {
showMessage(`Eliminando ${selectedPedimentoDocuments.length} documento(s)...`, 'info');
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedPedimentoDocuments
})
});
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 || selectedPedimentoDocuments.length} documento(s) eliminado(s) exitosamente`,
'success'
);
setSelectedPedimentoDocuments([]);
setIsSelectAllPedimentoDocs(false);
// Forzar recarga de documentos
const currentPage = pedimentoPage;
setPedimentoPage(0);
setTimeout(() => setPedimentoPage(currentPage), 100);
} catch (error) {
console.error('Error durante la eliminación masiva:', error);
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
// Resetear página cuando cambien los filtros de pedimento
useEffect(() => {
setPedimentoPage(1);
}, [pedimentoFilters]);
// Estados para Partidas
const [partidas, setPartidas] = useState([]);
const [partidasCount, setPartidasCount] = useState(0);
const [partidasLoading, setPartidasLoading] = useState(false);
const [partidasError, setPartidasError] = useState('');
const [partidasPage, setPartidasPage] = useState(1);
const [partidasPageSize, setPartidasPageSize] = useState(10);
const [partidasFilters, setPartidasFilters] = useState({
numero_partida: '',
descargado: '',
numero_partida__gte: '',
numero_partida__lte: '',
created_at__gte: '',
created_at__lte: ''
});
const [selectedPartidas, setSelectedPartidas] = useState([]);
const [downloadingPartidas, setDownloadingPartidas] = useState(false);
const [downloadingAllPartidas, setDownloadingAllPartidas] = useState(false);
// Estados para credenciales VUCEM
const [credenciales, setCredenciales] = useState([]);
const [processingPartida, setProcessingPartida] = useState(null);
const [processingCove, setProcessingCove] = useState(null);
const [processingAcuseCove, setProcessingAcuseCove] = useState(null);
const [processingEdoc, setProcessingEdoc] = useState(null);
const [processingAcuseEdoc, setProcessingAcuseEdoc] = useState(null);
// Agregar estado para el modal de documentos
const [showDocumentsModal, setShowDocumentsModal] = useState(false);
const [selectedVUDocuments, setSelectedVUDocuments] = useState([]);
const [selectedVUNumber, setSelectedVUNumber] = useState('');
// Función para manejar la visualización de documentos COVE
const handleShowCoveDocuments = (cove) => {
if (cove.documentos && cove.documentos.length > 0) {
setSelectedVUDocuments(cove.documentos);
setSelectedVUNumber(cove.numero_cove);
setShowDocumentsModal(true);
}
};
// Función para manejar la visualización de documentos COVE
const handleShowPartidaDocuments = (partida) => {
if (partida.documentos && partida.documentos.length > 0) {
setSelectedVUDocuments(partida.documentos);
setSelectedVUNumber(partida.numero_partida);
setShowDocumentsModal(true);
}
};
// Función para manejar la visualización de Edocuments
const handleShowEDocuments = (edocument) => {
if (edocument.documentos && edocument.documentos.length > 0) {
setSelectedVUDocuments(edocument.documentos);
setSelectedVUNumber(edocument.numero_edocument);
setShowDocumentsModal(true);
}
};
// Función para obtener el nombre de pestaña legible
const getTabName = (tabKey) => {
const tabNames = {
'pedimento': 'Pedimento',
'partidas': 'Partida',
'coves': 'COVE',
'edocs': 'EDoc',
'documentos': 'Documentos',
'auditor': 'Auditor'
};
return tabNames[tabKey] || tabKey;
};
// Función para obtener el resumen del dashboard
const fetchDashboardSummary = async () => {
try {
if (!pedimento) return;
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/dashboard/summary/?organizacion_id=${pedimento.organizacion}&pedimento_app=${pedimento.pedimento_app}`;
const res = await fetchWithAuth(url);
if (!res.ok) {
throw new Error('Error al obtener el resumen del dashboard');
}
const data = await res.json();
setDashboardSummary(data);
} catch (err) {
console.error('Error obteniendo el resumen del dashboard:', err);
showMessage('Error al obtener el resumen del dashboard: ' + err.message, 'error');
}
};
// Función para obtener credenciales VUCEM
const fetchCredenciales = async (contribuyente) => {
try {
const response = await fetchWithAuth(`${API_URL}/vucem/vucem/?importador=${contribuyente}`);
if (response.ok) {
const data = await response.json();
// La API devuelve un array directamente, no un objeto con results
const credenciales = Array.isArray(data) ? data : (data.results || []);
return credenciales;
}
return [];
} catch (error) {
console.error('Error fetching credenciales:', error);
return [];
}
};
// Función para procesar partida
const handlePartidaProcess = async (partida) => {
setProcessingPartida(partida.id);
try {
// Obtener credenciales para el contribuyente del pedimento
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
if (credencialesList.length === 0) {
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
return;
}
// Usar la primera credencial activa disponible
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
const requestBody = {
partida: {
id: partida.id,
numero: partida.numero_partida
},
pedimento: {
id: pedimento.id,
pedimento: pedimento.pedimento,
pedimento_app: pedimento.pedimento_app,
aduana: pedimento.aduana,
patente: pedimento.patente,
organizacion: pedimento.organizacion,
regimen: pedimento.regimen || "test",
clave_pedimento: pedimento.clave_pedimento || "test",
numero_operacion: pedimento.numero_operacion || ""
},
credencial: {
id: credencial.id,
user: credencial.usuario,
password: credencial.password,
efirma: credencial.efirma,
// Convertir URLs completas a rutas relativas
key: credencial.key ? credencial.key.split('/').slice(-2).join('/') : '',
cer: credencial.cer ? credencial.cer.split('/').slice(-2).join('/') : '',
is_active: credencial.is_active,
organizacion: credencial.organizacion
}
};
// Verificar si MICROSERVICE_URL_2 está definido
if (!MICROSERVICE_URL_2) {
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
}
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/partida/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
showMessage('Partida procesada correctamente', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Error al procesar la partida');
}
} catch (error) {
console.error('Error procesando partida:', error);
showMessage(`Error al procesar la partida: ${error.message}`, 'error');
} finally {
setProcessingPartida(null);
}
};
// Función para obtener partidas
const fetchPedimentoPartidas = async (page = 1, pageSize = 10, filters = {}) => {
console.log('fetchPedimentoPartidas called with:', { page, pageSize, filters });
console.log('pedimento object:', pedimento);
console.log('pedimento.id:', pedimento?.id);
if (!pedimento?.id) {
throw new Error('No hay ID de pedimento disponible');
}
const params = new URLSearchParams({
pedimento: pedimento.id,
page: page.toString(),
page_size: pageSize.toString()
});
// Solo agregar filtros que tengan valores
Object.entries(filters).forEach(([key, value]) => {
if (value && value.toString().trim() !== '') {
params.append(key, value);
}
});
const finalUrl = `${API_URL}/customs/partidas/?${params}`;
console.log('Final URL:', finalUrl);
const response = await fetchWithAuth(finalUrl);
if (!response.ok) {
throw new Error('Error al obtener las partidas');
}
const result = await response.json();
console.log('API Response:', result);
return result;
};
// Estados para Procesos
const [procesos, setProcesos] = useState([]);
const [procesosCount, setProcesosCount] = useState(0);
const [procesosLoading, setProcesosLoading] = useState(false);
const [procesosError, setProcesosError] = useState('');
const [procesosPage, setProcesosPage] = useState(1);
const [procesosPageSize, setProcesosPageSize] = useState(10);
const [procesosFilters, setProcesosFilters] = useState({});
const [sortField, setSortField] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
const [selectedProcesos, setSelectedProcesos] = useState([]);
const [isSelectAll, setIsSelectAll] = useState(false);
// Estados para las acciones de procesos
const [executingId, setExecutingId] = useState(null);
const [changingStateId, setChangingStateId] = useState(null);
const [creatingService, setCreatingService] = useState(null);
// Ref para rastrear valores previos de filtros
const prevFiltersRef = useRef({
sortField: '',
sortOrder: 'asc'
});
// Estados para modal de preview
const [previewOpen, setPreviewOpen] = useState(false);
const [previewUrl, setPreviewUrl] = useState('');
const [previewType, setPreviewType] = useState('');
const [previewLoading, setPreviewLoading] = useState(false);
const [previewError, setPreviewError] = useState('');
const [previewXml, setPreviewXml] = useState('');
const [previewXmlHtml, setPreviewXmlHtml] = useState('');
const [previewDoc, setPreviewDoc] = useState(null);
const [previewContent, setPreviewContent] = useState('');
const [imageZoom, setImageZoom] = useState(1);
// Refs
const focusKeeperRef = useRef(null);
// Opciones para tipos de documento
const documentTypeOptions = [
{ value: '', label: 'Todos' },
{ value: 1, label: 'Pedimento Partida' },
{ value: 2, label: 'Pedimento Completo' },
{ value: 3, label: 'Remesa' },
{ value: 4, label: 'Acuse' },
{ value: 5, label: 'EDocument' },
{ value: 7, label: 'Acuse Cove' },
{ value: 8, label: 'Cove' },
{ value: 9, label: 'Documento de digitalización' },
];
// Helper para obtener el nombre legible del tipo de documento
const getDocumentTypeName = (type) => {
const found = documentTypeOptions.find(opt => String(opt.value) === String(type));
return found ? found.label : 'Documento';
};
// Función para cambiar de pestaña
const handleTabChange = (tab) => {
setActiveTab(tab);
if (tab === 'auditor') {
fetchDashboardSummary();
}
};
// Handler SPA para paginación
const handlePageChange = (newPage, e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
if (newPage < 1 || newPage > Math.max(1, Math.ceil(docsCount / pageSize)) || newPage === page) return;
setPage(newPage);
// Quitar el foco del botón activo para evitar salto de scroll
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
// Handler para cambio de ordenamiento
const handleSort = (field) => {
if (orderBy === field) {
setOrderDir(orderDir === 'asc' ? 'desc' : 'asc');
} else {
setOrderBy(field);
setOrderDir('asc');
}
};
// Funciones de selección
const allDocIds = documents.map(doc => doc.id);
const allSelected = selected.length === allDocIds.length && allDocIds.length > 0;
const handleSelect = (id) => {
setSelected(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
const handleSelectAllDocs = () => {
if (allSelected) setSelected([]);
else setSelected(allDocIds);
};
const handleBulkDownload = async (ids) => {
setDownloading(true);
await downloadBulkZip(ids, showMessage, pedimento?.pedimento);
setDownloading(false);
};
// Vista previa de documento
const handlePreviewVU = async (doc) => {
setPreviewLoading(true);
setPreviewError('');
setPreviewUrl('');
setPreviewType('');
setPreviewXml('');
setPreviewDoc(doc);
setImageZoom(1);
setPreviewContent('');
setPreviewOpen(true);
try {
const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${doc.id}/`);
if (!res.ok) {
setPreviewError('Error al obtener el archivo');
setPreviewLoading(false);
return;
}
// Detectar tipo de archivo
let type = '';
if (doc.extension) {
if (doc.extension.toLowerCase() === 'pdf') type = 'pdf';
else if (["jpg","jpeg","png","gif","bmp","webp"].includes(doc.extension.toLowerCase())) type = 'img';
else if (doc.extension.toLowerCase() === 'xml') type = 'xml';
else if (["txt","log","csv"].includes(doc.extension.toLowerCase())) type = 'txt';
else type = 'other';
}
setPreviewType(type);
if (type === 'xml') {
const text = await res.text();
const prettyText = formatXml(text);
setPreviewXml(prettyText);
// Formatear y resaltar XML
try {
const highlighted = hljs.highlight(prettyText, { language: 'xml' }).value;
setPreviewXmlHtml(highlighted);
} catch (e) {
setPreviewXmlHtml(prettyText);
}
setPreviewLoading(false);
} else if (type === 'txt') {
const text = await res.text();
setPreviewContent(text);
setPreviewLoading(false);
} else {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
setPreviewUrl(url);
setPreviewLoading(false);
}
} catch (err) {
console.error('Error in preview:', err);
if (err.message === 'SESSION_EXPIRED') {
setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.');
} else {
setPreviewError('Error al obtener el archivo');
}
setPreviewLoading(false);
}
};
// Vista previa de documento
const handlePreview = async (doc) => {
setPreviewLoading(true);
setPreviewError('');
setPreviewUrl('');
setPreviewType('');
setPreviewXml('');
setPreviewDoc(doc);
setImageZoom(1);
setPreviewContent('');
setPreviewOpen(true);
try {
const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${doc.id}/`);
if (!res.ok) {
setPreviewError('Error al obtener el archivo');
setPreviewLoading(false);
return;
}
// Detectar tipo de archivo
let type = '';
if (doc.extension) {
if (doc.extension.toLowerCase() === 'pdf') type = 'pdf';
else if (["jpg","jpeg","png","gif","bmp","webp"].includes(doc.extension.toLowerCase())) type = 'img';
else if (doc.extension.toLowerCase() === 'xml') type = 'xml';
else if (["txt","log","csv"].includes(doc.extension.toLowerCase())) type = 'txt';
else type = 'other';
}
setPreviewType(type);
if (type === 'xml') {
const text = await res.text();
const prettyText = formatXml(text);
setPreviewXml(prettyText);
// Formatear y resaltar XML
try {
const highlighted = hljs.highlight(prettyText, { language: 'xml' }).value;
setPreviewXmlHtml(highlighted);
} catch (e) {
setPreviewXmlHtml(prettyText);
}
setPreviewLoading(false);
} else if (type === 'txt') {
const text = await res.text();
setPreviewContent(text);
setPreviewLoading(false);
} else {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
setPreviewUrl(url);
setPreviewLoading(false);
}
} catch (err) {
console.error('Error in preview:', err);
if (err.message === 'SESSION_EXPIRED') {
setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.');
} else {
setPreviewError('Error al obtener el archivo');
}
setPreviewLoading(false);
}
};
// Cerrar modal y limpiar blob
const handleClosePreview = () => {
setPreviewOpen(false);
if (previewUrl) window.URL.revokeObjectURL(previewUrl);
setPreviewUrl('');
setPreviewType('');
setPreviewError('');
setPreviewXml('');
setPreviewXmlHtml('');
setPreviewDoc(null);
setPreviewContent('');
setImageZoom(1);
};
// Funciones para el nuevo diseño de documentos
const downloadAll = async () => {
setDownloadingAll(true);
try {
const allDocIds = documents.map(doc => doc.id);
await handleBulkDownload(allDocIds);
} catch (error) {
console.error('Error downloading all documents:', error);
showMessage('Error al descargar todos los documentos', 'error');
} finally {
setDownloadingAll(false);
}
};
// Funciones para el nuevo diseño de documentos
const downloadAllPedimento = async () => {
setDownloadingAllPedimento(true);
try {
const allDocIds = peddocuments.map(doc => doc.id);
await handleBulkDownload(allDocIds);
} catch (error) {
console.error('Error downloading all documents:', error);
showMessage('Error al descargar todos los documentos', 'error');
} finally {
setDownloadingAllPedimento(false);
}
};
const previewDocumentVU = async (doc) => {
// Cerrar el modal primero
setShowDocumentsModal(false);
setSelectedVUDocuments([]);
setSelectedVUNumber('');
await handlePreviewVU(doc);
};
const downloadAllVU = async () => {
setDownloadingAll(true);
try {
const allDocIds = selectedVUDocuments.map(doc => doc.id);
await handleBulkDownload(allDocIds);
} catch (error) {
console.error('Error downloading all documents:', error);
showMessage('Error al descargar todos los documentos', 'error');
} finally {
setDownloadingAll(false);
}
};
const downloadDocumentVU = async (doc) => {
const fileName = doc.archivo ? doc.archivo.split('/').pop() : `documento_${doc.id}`;
await downloadFile(doc.id, fileName, showMessage);
};
const previewDocument = async (doc) => {
await handlePreview(doc);
};
const downloadDocument = async (doc) => {
const fileName = doc.archivo ? doc.archivo.split('/').pop() : `documento_${doc.id}`;
await downloadFile(doc.id, fileName, showMessage);
};
const formatFileSize = (bytes) => {
if (!bytes) return 'N/A';
const kb = bytes / 1024;
if (kb < 1024) {
return `${kb.toFixed(1)} KB`;
}
const mb = kb / 1024;
return `${mb.toFixed(1)} MB`;
};
const getFileExtension = (filename) => {
if (!filename) return '';
const parts = filename.split('.');
return parts.length > 1 ? parts[parts.length - 1] : '';
};
const getFuenteName = (fuente) => {
const fuentes = {
1: 'Manual',
2: 'VU',
3: 'Importación',
4: 'Sistema',
6: 'APP-EFC'
};
return fuentes[fuente] || 'Desconocida';
};
// Funciones para manejo de selección múltiple de documentos
const handleSelectDocument = (documentId) => {
const isSelected = selectedDocuments.includes(documentId);
if (isSelected) {
setSelectedDocuments(prev => prev.filter(id => id !== documentId));
} else {
setSelectedDocuments(prev => [...prev, documentId]);
}
};
const handleSelectAllDocuments = () => {
if (isSelectAllDocs) {
setSelectedDocuments([]);
setIsSelectAllDocs(false);
} else {
const allDocumentIds = documents.map(doc => doc.id);
setSelectedDocuments(allDocumentIds);
setIsSelectAllDocs(true);
}
};
// Función para eliminar documentos seleccionados
const handleDeleteSelectedDocuments = 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 confirmDeleteDocuments = 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}/record/documents/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([]);
setIsSelectAllDocs(false);
// Forzar recarga de documentos cambiando la página temporalmente
const currentPage = page;
setPage(0); // Cambio temporal
setTimeout(() => setPage(currentPage), 100); // Restaurar después de un breve delay
} catch (error) {
console.error('Error durante la eliminación masiva:', error);
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
// 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) {
const allSelected = documents.every(doc => selectedDocuments.includes(doc.id));
setIsSelectAllDocs(allSelected && selectedDocuments.length > 0);
}
}, [selectedDocuments, documents]);
// Efecto para limpiar selección cuando cambia de página
useEffect(() => {
setSelectedDocuments([]);
setIsSelectAllDocs(false);
}, [page]);
// Efecto para cargar datos del pedimento
useEffect(() => {
const fetchPedimento = async () => {
try {
const response = await fetchWithAuth(`${API_URL}/customs/pedimentos/${id}/`);
if (response.ok) {
const data = await response.json();
setPedimento(data);
}
} catch (err) {
setError('Error al cargar el pedimento');
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
}
} finally {
setLoading(false);
}
};
if (id) {
fetchPedimento();
}
}, [id, showMessage]);
// Fetch paginated documents
useEffect(() => {
if (!id) return;
console.log('Starting to fetch documents with:', { id, page, pageSize, filters, orderBy, orderDir });
setDocsLoading(true);
setDocsError('');
// Construir parámetros de filtros usando el nuevo sistema
const apiFilters = {};
if (filters.document_type) apiFilters.document_type = filters.document_type;
if (filters.name) apiFilters.archivo__icontains = filters.name;
if (filters.extension) apiFilters.extension = filters.extension;
if (filters.date) apiFilters.created_at__date = filters.date;
if (filters.created_at__gte) apiFilters.created_at__gte = filters.created_at__gte;
if (filters.created_at__lte) apiFilters.created_at__lte = filters.created_at__lte;
if (filters.fuente) apiFilters.fuente = filters.fuente;
if (filters.pedimento_numero) apiFilters.pedimento_numero__icontains = filters.pedimento_numero;
// Filtro para identificar modulo de pedimento
apiFilters.modulo = 'expedientes-detalle-pedimentos';
// Mantener compatibilidad con filtros legacy si están en uso
if (documentTypeFilter) apiFilters.document_type = documentTypeFilter;
if (fileNameFilter) apiFilters.archivo__icontains = fileNameFilter;
if (extensionFilter) apiFilters.extension = extensionFilter;
if (dateFilter) apiFilters.created_at__date = dateFilter;
if (orderBy) {
const orderField = orderBy === 'archivo' ? 'archivo' :
orderBy === 'document_type' ? 'document_type' :
orderBy === 'created_at' ? 'created_at' :
orderBy === 'size' ? 'size' : orderBy;
apiFilters.ordering = orderDir === 'desc' ? `-${orderField}` : orderField;
}
console.log('Calling fetchPedimentoDocuments with filters:', apiFilters);
fetchPedimentoDocuments(id, page, pageSize, apiFilters)
.then((data) => {
console.log('Received documents data:', data);
setDocuments(data.results);
setDocsCount(data.count);
setDocsNext(data.next);
setDocsPrev(data.previous);
setDocsLoading(false);
})
.catch(err => {
console.error('Error fetching documents:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setDocsError(err.message);
}
setDocsLoading(false);
});
}, [id, page, pageSize, filters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir, showMessage]);
// Resetear página cuando cambien los filtros
useEffect(() => {
setPage(1);
}, [filters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter]);
// Debug logs
useEffect(() => {
console.log('Documents data:', { documents, docsCount, page, pageSize, docsLoading, docsError });
}, [documents, docsCount, page, pageSize, docsLoading, docsError]);
// Fetch Pedimento T2025-10-152 documents
useEffect(() => {
if (!id || activeTab !== 'pedimento') return;
setPedimentoLoading(true);
setPedimentoError('');
// Construir parámetros de filtros para pedimento completo
const apiFilters = {};
if (pedimentoFilters.document_type) apiFilters.document_type = pedimentoFilters.document_type;
if (pedimentoFilters.name) apiFilters.archivo__icontains = pedimentoFilters.name;
if (pedimentoFilters.extension) apiFilters.extension = pedimentoFilters.extension;
if (pedimentoFilters.date) apiFilters.created_at__date = pedimentoFilters.date;
if (pedimentoFilters.created_at__gte) apiFilters.created_at__gte = pedimentoFilters.created_at__gte;
if (pedimentoFilters.created_at__lte) apiFilters.created_at__lte = pedimentoFilters.created_at__lte;
if (pedimentoFilters.fuente) apiFilters.fuente = pedimentoFilters.fuente;
if (pedimentoFilters.pedimento_numero) apiFilters.pedimento_numero__icontains = pedimentoFilters.pedimento_numero;
// Mantener compatibilidad con filtros legacy si están en uso
if (documentTypeFilter) apiFilters.document_type = documentTypeFilter;
if (fileNameFilter) apiFilters.archivo__icontains = fileNameFilter;
if (extensionFilter) apiFilters.extension = extensionFilter;
if (dateFilter) apiFilters.created_at__date = dateFilter;
if (orderBy) {
const orderField = orderBy === 'archivo' ? 'archivo' :
orderBy === 'document_type' ? 'document_type' :
orderBy === 'created_at' ? 'created_at' :
orderBy === 'size' ? 'size' : orderBy;
apiFilters.ordering = orderDir === 'desc' ? `-${orderField}` : orderField;
}
fetchPedimentoCompleto(id, pedimentoPage, pedimentoPageSize,apiFilters)
.then((data) => {
setPedDocuments(data.results);
setPedDocsCount(data.count);
setPedimentoNext(data.next);
setPedimentoPrev(data.previous);
setPedimentoLoading(false);
})
.catch(err => {
console.error('Error fetching Pedimento Completo:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setPedimentoError(err.message);
}
setPedimentoLoading(false);
});
},[id, activeTab, pedimentoPage, pedimentoPageSize, showMessage, pedimentoFilters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir]);
// Fetch COVEs cuando sea necesario
useEffect(() => {
if (!id || activeTab !== 'coves') return;
setCovesLoading(true);
setCovesError('');
fetchPedimentoCoves(id, covesPage, covesPageSize, covesFilters)
.then((data) => {
setCoves(data.results);
setCovesCount(data.count);
setCovesLoading(false);
})
.catch(err => {
console.error('Error fetching COVEs:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setCovesError(err.message);
}
setCovesLoading(false);
});
}, [id, activeTab, covesPage, covesPageSize, covesFilters, showMessage]);
// Resetear página de COVEs cuando cambien los filtros
useEffect(() => {
setCovesPage(1);
}, [covesFilters]);
// Fetch EDocs cuando sea necesario
useEffect(() => {
if (!id || activeTab !== 'edocs') return;
setEdocsLoading(true);
setEdocsError('');
fetchPedimentoEdocuments(id, edocsPage, edocsPageSize, edocsFilters)
.then((data) => {
setEdocs(data.results);
setEdocsCount(data.count);
setEdocsLoading(false);
})
.catch(err => {
console.error('Error fetching EDocs:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setEdocsError(err.message);
}
setEdocsLoading(false);
});
}, [id, activeTab, edocsPage, edocsPageSize, edocsFilters, showMessage]);
// Resetear página de EDocs cuando cambien los filtros
useEffect(() => {
setEdocsPage(1);
}, [edocsFilters]);
// Funciones para acciones de Procesos
// Map legacy estado values to new status values
const mapEstadoToStatus = (estado) => {
const mapping = {
1: 'pending', // Pendiente
2: 'processing', // En Proceso
3: 'completed', // Completado
4: 'failed', // Error
5: 'cancelled' // Cancelado
};
return mapping[estado] || 'pending';
};
const updateProcesoEstado = (procId, nuevoEstado) => {
setProcesos(procesos =>
procesos.map(proc =>
proc.id === procId ? { ...proc, status: mapEstadoToStatus(nuevoEstado) } : proc
)
);
};
// Verificar si existe un servicio específico
const existeServicio = (tipoServicio) => {
return procesos.some(proceso => proceso.servicio === tipoServicio);
};
// Función para crear un nuevo servicio
const handleCrearServicio = async (tipoServicio, nombreServicio) => {
setCreatingService(tipoServicio);
try {
const body = {
pedimento: id,
servicio: tipoServicio,
estado: 1, // Pendiente
tipo_procesamiento: 1,
organizacion: pedimentoData?.organizacion || 1 // Usar la organización del pedimento o default
};
const res = await postWithAuth(`${API_URL}/tasks/tasks/`, body);
if (!res.ok) {
throw new Error(`Error al crear el servicio: ${nombreServicio}`);
}
const nuevoServicio = await res.json();
// Agregar el nuevo servicio a la lista local
setProcesos(prev => [...prev, nuevoServicio]);
setProcesosCount(prev => prev + 1);
showMessage(`Servicio "${nombreServicio}" creado correctamente`, 'success');
// Refrescar la lista después de un breve delay
setTimeout(() => {
// Re-fetch los procesos para asegurar consistencia
const filters = {
...procesosFilters,
pedimento: id
};
fetchTasks(procesosPage, procesosPageSize, filters)
.then((data) => {
setProcesos(data.results);
setProcesosCount(data.count);
})
.catch(err => {
console.error('Error refreshing procesos:', err);
});
}, 1000);
} catch (err) {
console.error('Error creando servicio:', err);
showMessage(`Error al crear el servicio: ${err.message}`, 'error');
} finally {
setCreatingService(null);
}
};
const handlePasarAEspera = async (proc) => {
setChangingStateId(proc.id);
// Cambiar estado visual a "Procesando" inmediatamente
updateProcesoEstado(proc.id, 'processing');
try {
const body = {
status: 'pending', // Cambiar a Pendiente
tipo_procesamiento: 2,
pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento,
servicio: proc.servicio,
organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId
};
const res = await putWithAuth(`${API_URL}/tasks/tasks/${proc.task_id}/`, body);
if (!res.ok) {
// Si falla, cambiar a estado Error
updateProcesoEstado(proc.id, 'failed'); // Failed
throw new Error('Error al cambiar el estado del proceso');
}
// Cambiar estado visual a "En Espera" si fue exitoso
updateProcesoEstado(proc.id, 'pending');
showMessage('Estado cambiado a "En Espera" correctamente', 'success');
} catch (err) {
console.error('Error cambiando estado:', err);
updateProcesoEstado(proc.id, 'failed');
showMessage('Error al cambiar el estado del proceso: ' + err.message, 'error');
} finally {
setChangingStateId(null);
}
};
const handleEjecutarServicio = async (proc) => {
setExecutingId(proc.id);
// Cambiar estado visual a "Procesando" inmediatamente
updateProcesoEstado(proc.id, 'processing');
let endpoint = '';
// Determinar endpoint según el tipo de servicio
switch (proc.servicio) {
case 3:
endpoint = '/services/pedimento_completo';
break;
case 4: // Partidas
endpoint = '/services/partidas';
break;
case 5: // Remesas
endpoint = '/services/remesas';
break;
case 6: // Acuse
endpoint = '/services/acuse';
break;
case 7:
endpoint = '/services/edocument';
break;
case 8: // Coves
endpoint = '/services/coves';
break;
case 9: // Acuse Cove
endpoint = '/services/acuseCove';
break;
default:
// Revertir estado si el servicio no es soportado
updateProcesoEstado(proc.id, proc.status); // Revertir al estado original
showMessage('Este servicio no es compatible para ejecución directa.', 'error');
setExecutingId(null);
return;
}
try {
const body = {
pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento,
organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId,
};
const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body);
if (!res.ok) {
// Si falla, cambiar estado a Error
updateProcesoEstado(proc.id, 'failed');
throw new Error('Error al ejecutar el servicio');
}
// Si es exitoso, cambiar estado a Finalizado
updateProcesoEstado(proc.id, 'completed');
showMessage('El servicio se ha ejecutado correctamente', 'success');
} catch (err) {
// Cambiar estado a Error en caso de excepción
updateProcesoEstado(proc.id, 'failed');
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al ejecutar servicio: ' + err.message, 'error');
}
} finally {
setExecutingId(null);
}
};
// Función para comparar Remesas vs COVEs en DB
const handleCompararRemesasCoves = async () => {
try {
showMessage('Iniciando comparación de Remesas vs COVEs...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
// Llamada al endpoint de comparación (ajustar según la API real)
const res = await postWithAuth(`${MICROSERVICE_URL}/services/comparar_remesas_coves`, body);
if (!res.ok) {
throw new Error('Error al realizar la comparación');
}
const resultado = await res.json();
// Mostrar resultado de la comparación
if (resultado.coincidencias) {
showMessage(`Comparación completada: ${resultado.mensaje || 'Remesas y COVEs coinciden correctamente'}`, 'success');
} else {
showMessage(`Discrepancias encontradas: ${resultado.mensaje || 'Se encontraron diferencias entre Remesas y COVEs'}`, 'warning');
}
// Opcional: mostrar detalles en consola para debugging
console.log('Resultado comparación Remesas vs COVEs:', resultado);
} catch (err) {
console.error('Error comparando Remesas vs COVEs:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al comparar Remesas vs COVEs: ' + err.message, 'error');
}
}
};
// Función para auditar Pedimento Completo
const handleAuditarPedimentoCompleto = async () => {
try {
showMessage('Iniciando auditoría del Pedimento Completo...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/auditar_pedimento_completo`, body);
if (!res.ok) {
throw new Error('Error al auditar el pedimento completo');
}
const resultado = await res.json();
if (resultado.auditoria_exitosa) {
showMessage(`Auditoría completada: ${resultado.mensaje || 'Pedimento completo auditado correctamente'}`, 'success');
} else {
showMessage(`Problemas encontrados: ${resultado.mensaje || 'Se encontraron inconsistencias en el pedimento'}`, 'warning');
}
console.log('Resultado auditoría Pedimento Completo:', resultado);
} catch (err) {
console.error('Error auditando Pedimento Completo:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al auditar Pedimento Completo: ' + err.message, 'error');
}
}
};
// Función para auditar EDocs
const handleAuditarEDocs = async () => {
try {
showMessage('Iniciando auditoría de EDocs...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/auditar_edocs`, body);
if (!res.ok) {
throw new Error('Error al auditar EDocs');
}
const resultado = await res.json();
if (resultado.auditoria_exitosa) {
showMessage(`Auditoría EDocs completada: ${resultado.mensaje || 'EDocs auditados correctamente'}`, 'success');
} else {
showMessage(`Problemas en EDocs: ${resultado.mensaje || 'Se encontraron inconsistencias en EDocs'}`, 'warning');
}
console.log('Resultado auditoría EDocs:', resultado);
} catch (err) {
console.error('Error auditando EDocs:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al auditar EDocs: ' + err.message, 'error');
}
}
};
// Función para auditar Partidas
const handleAuditarPartidas = async () => {
try {
showMessage('Iniciando auditoría de Partidas...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/auditar_partidas`, body);
if (!res.ok) {
throw new Error('Error al auditar Partidas');
}
const resultado = await res.json();
if (resultado.auditoria_exitosa) {
showMessage(`Auditoría Partidas completada: ${resultado.mensaje || 'Partidas auditadas correctamente'}`, 'success');
} else {
showMessage(`Problemas en Partidas: ${resultado.mensaje || 'Se encontraron inconsistencias en Partidas'}`, 'warning');
}
console.log('Resultado auditoría Partidas:', resultado);
} catch (err) {
console.error('Error auditando Partidas:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al auditar Partidas: ' + err.message, 'error');
}
}
};
// Función para auditar Acuses
const handleAuditarAcuses = async () => {
try {
showMessage('Iniciando auditoría de Acuses...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/auditar_acuses`, body);
if (!res.ok) {
throw new Error('Error al auditar Acuses');
}
const resultado = await res.json();
if (resultado.auditoria_exitosa) {
showMessage(`Auditoría Acuses completada: ${resultado.mensaje || 'Acuses auditados correctamente'}`, 'success');
} else {
showMessage(`Problemas en Acuses: ${resultado.mensaje || 'Se encontraron inconsistencias en Acuses'}`, 'warning');
}
console.log('Resultado auditoría Acuses:', resultado);
} catch (err) {
console.error('Error auditando Acuses:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al auditar Acuses: ' + err.message, 'error');
}
}
};
// Función para auditar COVEs
const handleAuditarCoves = async () => {
try {
showMessage('Iniciando auditoría de COVEs...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/auditar_coves`, body);
if (!res.ok) {
throw new Error('Error al auditar COVEs');
}
const resultado = await res.json();
if (resultado.auditoria_exitosa) {
showMessage(`Auditoría COVEs completada: ${resultado.mensaje || 'COVEs auditados correctamente'}`, 'success');
} else {
showMessage(`Problemas en COVEs: ${resultado.mensaje || 'Se encontraron inconsistencias en COVEs'}`, 'warning');
}
console.log('Resultado auditoría COVEs:', resultado);
} catch (err) {
console.error('Error auditando COVEs:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al auditar COVEs: ' + err.message, 'error');
}
}
};
// Función para auditar Acuse de COVEs
const handleAuditarAcuseCoves = async () => {
try {
showMessage('Iniciando auditoría de Acuse de COVEs...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/auditar_acuse_coves`, body);
if (!res.ok) {
throw new Error('Error al auditar Acuse de COVEs');
}
const resultado = await res.json();
if (resultado.auditoria_exitosa) {
showMessage(`Auditoría Acuse COVEs completada: ${resultado.mensaje || 'Acuse de COVEs auditado correctamente'}`, 'success');
} else {
showMessage(`Problemas en Acuse COVEs: ${resultado.mensaje || 'Se encontraron inconsistencias en Acuse de COVEs'}`, 'warning');
}
console.log('Resultado auditoría Acuse COVEs:', resultado);
} catch (err) {
console.error('Error auditando Acuse COVEs:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al auditar Acuse COVEs: ' + err.message, 'error');
}
}
};
// Funciones de manejo de procesos
// Removed duplicate definitions
// Handle select all process checkboxes
const handleSelectAllProcesos = (e) => {
if (e.target.checked) {
const availableProcesos = procesos
.filter(proc => proc.status !== 'running' && proc.status !== 'completed')
.map(proc => proc.task_id);
setSelectedProcesos(availableProcesos);
setIsSelectAll(true);
} else {
setSelectedProcesos([]);
setIsSelectAll(false);
}
};
// Handle select individual process checkbox
const handleSelectProceso = (id, checked) => {
if (checked) {
setSelectedProcesos(prev => [...prev, id]);
} else {
setSelectedProcesos(prev => prev.filter(i => i !== id));
}
// Update isSelectAll
const availableProcesos = procesos
.filter(proc => proc.status !== 'running' && proc.status !== 'completed')
.map(proc => proc.task_id);
setIsSelectAll(availableProcesos.length === selectedProcesos.length + (checked ? 1 : -1));
};
const handlePasarPaginaAEspera = async () => {
const failedProcesses = procesos.filter(proc => proc.status === 'failed');
if (failedProcesses.length === 0) return;
for (const proceso of failedProcesses) {
await handlePasarAEspera(proceso);
}
};
const handlePasarSeleccionadosAEspera = async () => {
const failedSelectedProcesses = procesos.filter(
proc => selectedProcesos.includes(proc.task_id) && proc.status === 'failed'
);
if (failedSelectedProcesses.length === 0) return;
for (const proceso of failedSelectedProcesses) {
await handlePasarAEspera(proceso);
}
};
// Función para verificar servicios creados
const handleVerificarServiciosCreados = async () => {
try {
showMessage('Iniciando verificación de servicios creados...', 'info');
const body = {
pedimento: id,
organizacion: pedimento?.organizacion || 1
};
const res = await postWithAuth(`${MICROSERVICE_URL}/services/verificar_servicios_creados`, body);
if (!res.ok) {
throw new Error('Error al verificar servicios creados');
}
const resultado = await res.json();
if (resultado.verificacion_exitosa) {
showMessage(`Verificación completada: ${resultado.mensaje || 'Todos los servicios han sido verificados correctamente'}`, 'success');
} else {
showMessage(`Problemas encontrados: ${resultado.mensaje || 'Se encontraron servicios faltantes o con problemas'}`, 'warning');
}
// Mostrar resumen de servicios si está disponible
if (resultado.resumen) {
console.log('Resumen de servicios:', resultado.resumen);
// Opcional: mostrar el resumen en una notificación adicional
if (resultado.resumen.total_servicios) {
showMessage(`Servicios encontrados: ${resultado.resumen.servicios_activos}/${resultado.resumen.total_servicios}`, 'info');
}
}
console.log('Resultado verificación servicios creados:', resultado);
} catch (err) {
console.error('Error verificando servicios creados:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', 'error');
} else {
showMessage('Error al verificar servicios creados: ' + err.message, 'error');
}
}
};
// Fetch Partidas cuando sea necesario
useEffect(() => {
console.log('Partidas useEffect triggered:', { id, activeTab, pedimento: pedimento?.id, partidasPage, partidasPageSize, partidasFilters });
if (!id || activeTab !== 'partidas' || !pedimento) {
console.log('Skipping partidas fetch due to missing conditions:', {
hasId: !!id,
isPartidasTab: activeTab === 'partidas',
hasPedimento: !!pedimento
});
return;
}
console.log('Starting partidas fetch...');
setPartidasLoading(true);
setPartidasError('');
fetchPedimentoPartidas(partidasPage, partidasPageSize, partidasFilters)
.then((data) => {
console.log('Partidas fetch success:', data);
setPartidas(data.results || []);
setPartidasCount(data.count || 0);
setPartidasLoading(false);
})
.catch(err => {
console.error('Error fetching Partidas:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setPartidasError(err.message);
}
setPartidasLoading(false);
});
}, [id, activeTab, partidasPage, partidasPageSize, partidasFilters, showMessage, pedimento]);
// Resetear página de Partidas cuando cambien los filtros
useEffect(() => {
setPartidasPage(1);
}, [partidasFilters]);
// Fetch Procesos cuando sea necesario
useEffect(() => {
if (!id || activeTab !== 'procesos') return;
// Detectar si algún filtro cambió
const currentFilters = {
sortField,
sortOrder
};
const filtersChanged = Object.keys(currentFilters).some(
key => currentFilters[key] !== prevFiltersRef.current[key]
);
// Si los filtros cambiaron y no estamos en la página 1, resetear página
if (filtersChanged && procesosPage !== 1) {
setProcesosPage(1);
// Actualizar ref con valores actuales
prevFiltersRef.current = { ...currentFilters };
return; // Salir temprano, el efecto se ejecutará de nuevo con page = 1
}
// Actualizar ref con valores actuales
prevFiltersRef.current = { ...currentFilters };
setProcesosLoading(true);
setProcesosError('');
// Construir filtros
const filters = {
...procesosFilters,
pedimento: id // Filtrar por el pedimento actual
};
if (sortField) {
const fieldMapping = {
'id': 'task_id',
'estado': 'status',
'pedimento': 'pedimento_app'
};
const mappedField = fieldMapping[sortField] || sortField;
filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + mappedField;
}
fetchTasks(procesosPage, procesosPageSize, filters)
.then((data) => {
setProcesos(data.results);
setProcesosCount(data.count);
setProcesosLoading(false);
})
.catch(err => {
console.error('Error fetching Procesos:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setProcesosError(err.message);
}
setProcesosLoading(false);
});
}, [id, activeTab, procesosPage, procesosPageSize, sortField, sortOrder, procesosFilters, showMessage]);
// Resetear página de Procesos cuando cambie el pedimento
useEffect(() => {
setProcesosPage(1);
}, [id]);
// Funciones para COVEs
const handleCoveDownload = async (cove) => {
try {
await downloadCove(cove.id);
showMessage(`COVE ${cove.numero_cove} descargado exitosamente`, 'success');
} catch (error) {
showMessage(`Error al descargar COVE: ${error.message}`, 'error');
}
};
const handleAcuseCoveDownload = async (cove) => {
try {
await downloadAcuseCove(cove.id);
showMessage(`Acuse de COVE ${cove.numero_cove} descargado exitosamente`, 'success');
} catch (error) {
showMessage(`Error al descargar acuse de COVE: ${error.message}`, 'error');
}
};
// Funciones para EDocs
const handleEdocDownload = async (edoc) => {
try {
await downloadEdocument(edoc.id);
showMessage(`EDocs ${edoc.numero_edocument} descargado exitosamente`, 'success');
} catch (error) {
showMessage(`Error al descargar EDocs: ${error.message}`, 'error');
}
};
const handleAcuseEdocDownload = async (edoc) => {
try {
await downloadAcuseEdocument(edoc.id);
showMessage(`Acuse de EDocs ${edoc.numero_edocument} descargado exitosamente`, 'success');
} catch (error) {
showMessage(`Error al descargar acuse de EDocs: ${error.message}`, 'error');
}
};
// Funciones para procesar peticiones
// Descargar todos los AcuseCoves
// Ejecutar la acción de procesar AcuseCove para todos los COVEs visibles
// Ejecutar la acción de procesar AcuseCove solo para los que no están descargados
const handleDownloadAllAcuseCoves = async () => {
if (!coves || coves.length === 0) {
showMessage('No hay AcuseCoves por descargar', 'info');
return;
}
const pendientes = coves.filter(cove => !cove.acuse_cove_descargado);
if (pendientes.length === 0) {
showMessage('No hay AcuseCoves pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const cove of pendientes) {
try {
await handleAcuseCoveProcess(cove);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) {
showMessage(`${successCount} AcuseCove(s) procesados exitosamente`, 'success');
}
if (errorCount > 0) {
showMessage(`${errorCount} AcuseCove(s) no se pudieron procesar`, 'error');
}
};
// Ejecutar la acción de procesar COVE solo para los que no están descargados
const handleDownloadAllCoves = async () => {
if (!coves || coves.length === 0) {
showMessage('No hay COVEs por descargar', 'info');
return;
}
const pendientes = coves.filter(cove => !cove.cove_descargado);
if (pendientes.length === 0) {
showMessage('No hay COVEs pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const cove of pendientes) {
try {
await handleCoveProcess(cove);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) {
showMessage(`${successCount} COVE(s) procesados exitosamente`, 'success');
}
if (errorCount > 0) {
showMessage(`${errorCount} COVE(s) no se pudieron procesar`, 'error');
}
};
const handleCoveRequest = async (cove) => {
console.log('Request cove:', cove);
showMessage(`Procesando petición para COVE #${cove.numero_cove}...`, 'info');
// Aquí implementarías la lógica de petición específica para COVEs
};
// Función para procesar COVE
const handleCoveProcess = async (cove) => {
setProcessingCove(cove.id);
try {
// Obtener credenciales para el contribuyente del pedimento
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
if (credencialesList.length === 0) {
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
return;
}
// Usar la primera credencial activa disponible
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
const requestBody = {
cove: {
id: cove.id,
cove: cove.numero_cove
},
pedimento: {
id: pedimento.id,
pedimento: pedimento.pedimento,
pedimento_app: pedimento.pedimento_app,
aduana: pedimento.aduana,
patente: pedimento.patente,
organizacion: pedimento.organizacion,
regimen: pedimento.regimen || "test",
clave_pedimento: pedimento.clave_pedimento || "test",
numero_operacion: pedimento.numero_operacion || "test"
},
credencial: {
id: credencial.id,
user: credencial.usuario,
password: credencial.password,
efirma: credencial.efirma,
key: credencial.key,
cer: credencial.cer,
is_active: credencial.is_active,
organizacion: credencial.organizacion
}
};
// Verificar si MICROSERVICE_URL_2 está definido
if (!MICROSERVICE_URL_2) {
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
}
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/cove/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
showMessage('COVE procesado correctamente', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Error al procesar el COVE');
}
} catch (error) {
console.error('Error procesando COVE:', error);
showMessage(`Error al procesar el COVE: ${error.message}`, 'error');
} finally {
setProcessingCove(null);
}
};
// Función para procesar Acuse de COVE
const handleAcuseCoveProcess = async (cove) => {
setProcessingAcuseCove(cove.id);
try {
// Obtener credenciales para el contribuyente del pedimento
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
if (credencialesList.length === 0) {
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
return;
}
// Usar la primera credencial activa disponible
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
const requestBody = {
cove: {
id: cove.id,
cove: cove.numero_cove
},
pedimento: {
id: pedimento.id,
pedimento: pedimento.pedimento,
pedimento_app: pedimento.pedimento_app,
aduana: pedimento.aduana,
patente: pedimento.patente,
organizacion: pedimento.organizacion,
regimen: pedimento.regimen || "test",
clave_pedimento: pedimento.clave_pedimento || "test",
numero_operacion: pedimento.numero_operacion || "test"
},
credencial: {
id: credencial.id,
user: credencial.usuario,
password: credencial.password,
efirma: credencial.efirma,
key: credencial.key,
cer: credencial.cer,
is_active: credencial.is_active,
organizacion: credencial.organizacion
}
};
// Verificar si MICROSERVICE_URL_2 está definido
if (!MICROSERVICE_URL_2) {
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
}
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/acuse/cove/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
showMessage('Acuse de COVE procesado correctamente', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Error al procesar el acuse de COVE');
}
} catch (error) {
console.error('Error procesando acuse de COVE:', error);
showMessage(`Error al procesar el acuse de COVE: ${error.message}`, 'error');
} finally {
setProcessingAcuseCove(null);
}
};
// Función para procesar EDoc
const handleEdocProcess = async (edoc) => {
setProcessingEdoc(edoc.id);
try {
// Obtener credenciales para el contribuyente del pedimento
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
if (credencialesList.length === 0) {
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
return;
}
// Usar la primera credencial activa disponible
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
const requestBody = {
edoc: {
id: edoc.id,
numero_edocument: edoc.numero_edocument
},
idEDocument: edoc.numero_edocument,
pedimento: {
id: pedimento.id,
pedimento: pedimento.pedimento,
pedimento_app: pedimento.pedimento_app,
aduana: pedimento.aduana,
patente: pedimento.patente,
organizacion: pedimento.organizacion,
regimen: pedimento.regimen || "test",
clave_pedimento: pedimento.clave_pedimento || "test",
numero_operacion: pedimento.numero_operacion || "test"
},
credencial: {
id: credencial.id,
user: credencial.usuario,
password: credencial.password,
efirma: credencial.efirma,
key: credencial.key,
cer: credencial.cer,
is_active: credencial.is_active,
organizacion: credencial.organizacion
}
};
// Verificar si MICROSERVICE_URL_2 está definido
if (!MICROSERVICE_URL_2) {
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
}
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/download/edoc/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
showMessage('EDoc procesado correctamente', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Error al procesar el EDoc');
}
} catch (error) {
console.error('Error procesando EDoc:', error);
showMessage(`Error al procesar el EDoc: ${error.message}`, 'error');
} finally {
setProcessingEdoc(null);
}
};
// Función para procesar Acuse de EDoc
const handleAcuseEdocProcess = async (edoc) => {
setProcessingAcuseEdoc(edoc.id);
try {
// Obtener credenciales para el contribuyente del pedimento
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
if (credencialesList.length === 0) {
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
return;
}
// Usar la primera credencial activa disponible
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
const requestBody = {
edoc: {
id: edoc.id,
numero_edocument: edoc.numero_edocument
},
idEDocument: edoc.numero_edocument,
pedimento: {
id: pedimento.id,
pedimento: pedimento.pedimento,
pedimento_app: pedimento.pedimento_app,
aduana: pedimento.aduana,
patente: pedimento.patente,
organizacion: pedimento.organizacion,
regimen: pedimento.regimen || "test",
clave_pedimento: pedimento.clave_pedimento || "test",
numero_operacion: pedimento.numero_operacion || "test"
},
credencial: {
id: credencial.id,
user: credencial.usuario,
password: credencial.password,
efirma: credencial.efirma,
key: credencial.key,
cer: credencial.cer,
is_active: credencial.is_active,
organizacion: credencial.organizacion
}
};
// Verificar si MICROSERVICE_URL_2 está definido
if (!MICROSERVICE_URL_2) {
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
}
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/acuse/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
showMessage('Acuse de EDoc procesado correctamente', 'success');
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Error al procesar el acuse de EDoc');
}
} catch (error) {
console.error('Error procesando acuse de EDoc:', error);
showMessage(`Error al procesar el acuse de EDoc: ${error.message}`, 'error');
} finally {
setProcessingAcuseEdoc(null);
}
};
const handleEdocRequest = async (edoc) => {
console.log('Request edoc:', edoc);
showMessage(`Procesando petición para EDocs #${edoc.numero_edocument}...`, 'info');
// Aquí implementarías la lógica de petición específica para EDocs
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// Funciones para Partidas
const handlePartidaPreview = async (partida) => {
// Pasar un objeto que simule un documento con el ID del pedimento
const partidaDoc = { ...partida, id: pedimento.id };
await handlePreview(partidaDoc);
};
const handlePartidaDownload = async (partida) => {
const fileName = partida.archivo ? partida.archivo.split('/').pop() : `partida_${partida.numero_partida}_${partida.id}`;
await downloadFile(pedimento.id, fileName, showMessage);
};
const handlePartidaRequest = async (partida) => {
await handlePartidaProcess(partida);
};
// Funciones de selección para partidas
const allPartidaIds = partidas.map(partida => partida.id);
const allPartidasSelected = selectedPartidas.length === allPartidaIds.length && allPartidaIds.length > 0;
const handleSelectPartida = (id) => {
setSelectedPartidas(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
const handleSelectAllPartidas = () => {
if (allPartidasSelected) setSelectedPartidas([]);
else setSelectedPartidas(allPartidaIds);
};
const handleBulkDownloadPartidas = async (ids) => {
setDownloadingPartidas(true);
await downloadBulkZip(ids, showMessage, `${pedimento?.pedimento}_partidas`);
setDownloadingPartidas(false);
};
// Función para eliminar documentos seleccionados de partida vu
const handleDeleteSelectedPartidaVU= async () => {
if (selectedPartidas.length === 0) {
showMessage('No hay documentos seleccionados para eliminar', 'warning');
return;
}
try {
showMessage(`Eliminando ${selectedPartidas.length} documento(s)...`, 'info');
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete-partidas-vu/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedPartidas
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || errorData?.error || `Error ${response.status}: ${response.statusText}`);
}
const result = await response.json();
showMessage(
`${result.deleted_count || selectedPartidas.length} documento(s) eliminado(s) exitosamente`,
'success'
);
setSelectedPartidas([]);
// Forzar recarga de documentos
const currentPage = partidasPage;
setPartidasPage(0);
setTimeout(() => setPartidasPage(currentPage), 100);
} catch (error) {
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
//--- Funciones de seleccion de Coves --- //
const [selectedCoves, setSelectedCoves] = useState([]);
const allCoveIds=coves.map(cove => cove.id);
const allCovesSelected=selectedCoves.length === allCoveIds.length && allCoveIds.length > 0;
const handleSelectAllCoves = () => {
if (allCovesSelected) setSelectedCoves([]);
else setSelectedCoves(allCoveIds);
};
const handleSelectCove = (id) => {
setSelectedCoves(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
// Función para eliminar documentos seleccionados de coves vu
const handleDeleteSelectedCovesVU= async () => {
if (selectedCoves.length === 0) {
showMessage('No hay documentos seleccionados para eliminar', 'warning');
return;
}
try {
showMessage(`Eliminando ${selectedCoves.length} documento(s)...`, 'info');
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete-coves-vu/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedCoves
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.message || errorData?.error || `Error ${response.status}: ${response.statusText}`);
}
const result = await response.json();
showMessage(
`${result.deleted_count || selectedCoves.length} documento(s) eliminado(s) exitosamente`,
'success'
);
setSelectedCoves([]);
// Forzar recarga de documentos
// const currentPage = covesPage;
setCovesPage(0);
setTimeout(() => setCovesPage(1), 100);
} catch (error) {
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
//--- Funciones seleccion de Pedimentos --- //
const [selectedPedimentos, setSelectedPedimentos] = useState([]);
const [isSelectAllPedimentos, setIsSelectAllPedimentos] = useState(false);
const allPedimentoIds=peddocuments.map(pedimento => pedimento.id);
const allPedimentosSelected=selectedPedimentos.length === allPedimentoIds.length && allPedimentoIds.length > 0;
const handleSelectAllPedimentos = () => {
if (allPedimentosSelected) {
setSelectedPedimentos([]);
setIsSelectAllPedimentos(false);
}
else{
setSelectedPedimentos(allPedimentoIds);
setIsSelectAllPedimentos(true);
}
};
const handleSelectPedimento = (id) => {
setSelectedPedimentos(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
// Función para eliminar documentos seleccionados de pedimento vu
const handleDeleteSelectedPedimentosVU= async () => {
if (selectedPedimentos.length === 0) {
showMessage('No hay documentos seleccionados para eliminar', 'warning');
return;
}
try {
showMessage(`Eliminando ${selectedPedimentos.length} documento(s)...`, 'info');
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedPedimentos
})
});
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 || selectedPedimentos.length} documento(s) eliminado(s) exitosamente`,
'success'
);
setSelectedPedimentos([]);
// Forzar recarga de documentos
const currentPage = pedimentoPage;
setPedimentoPage(0);
setTimeout(() => setPedimentoPage(currentPage), 100);
} catch (error) {
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
//--- Funciones seleccion de Edocuments --- //
const [selectedEdocs, setSelectedEdocs] = useState([]);
const allEdocIds=edocs.map(edoc => edoc.id);
const allEdocsSelected=selectedEdocs.length === allEdocIds.length && allEdocIds.length > 0;
const handleSelectAllEdocs = () => {
if (allEdocsSelected) setSelectedEdocs([]);
else setSelectedEdocs(allEdocIds);
};
const handleSelectEdoc = (id) => {
setSelectedEdocs(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
// Función para eliminar documentos seleccionados de partida vu
const handleDeleteSelectedEdocsVU= async () => {
if (selectedEdocs.length === 0) {
showMessage('No hay documentos seleccionados para eliminar', 'warning');
return;
}
try {
showMessage(`Eliminando ${selectedEdocs.length} documento(s)...`, 'info');
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete-edocs-vu/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedEdocs
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || errorData?.error || errorData?.message || `Error ${response.status}: ${response.statusText}`);
}
const result = await response.json();
showMessage(
`${result.deleted_count || selectedEdocs.length} documento(s) eliminado(s) exitosamente`,
'success'
);
setSelectedEdocs([]);
// Forzar recarga de documentos
const currentPage = edocsPage;
setEdocsPage(0);
setTimeout(() => setEdocsPage(currentPage), 100);
} catch (error) {
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
//
const downloadAllPartidas = async () => {
setDownloadingAllPartidas(true);
try {
const allPartidaIds = partidas.map(partida => partida.id);
await handleBulkDownloadPartidas(allPartidaIds);
} catch (error) {
console.error('Error downloading all partidas:', error);
showMessage('Error al descargar todas las partidas', 'error');
} finally {
setDownloadingAllPartidas(false);
}
};
//--- Carga de documentos a partidas ---//
// Estados adicionales para subir documentos a partida
const [uploadingToPartida, setUploadingToPartida] = useState(null);
const [selectedPartidaForUpload, setSelectedPartidaForUpload] = useState(null);
const [uploadingToDocumento, setUploadingToDocumento] = useState(null);
const [selectedDocumentForUpload, setSelectedDocumentForUpload] = useState(null);
const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [uploadFiles, setUploadFiles] = useState([]);
// Función para manejar la carga de documentos a una partida
const handleUploadToPartida = (partida) => {
console.log(partida);
// setSelectedPartidaForUpload(partida);
partida.tab = 'partida';
partida.numero = partida.numero_partida;
setSelectedDocumentForUpload(partida);
setUploadFiles([]);
setUploadModalOpen(true);
};
// Función para subir documentos a la partida seleccionada
const handleUploadFilesToPartida = async () => {
if (!selectedPartidaForUpload || uploadFiles.length === 0) {
showMessage('Por favor selecciona al menos un archivo', 'warning');
return;
}
setUploadingToPartida(selectedPartidaForUpload.id);
try{
const formData = new FormData();
// Agregar el ID de la partida
formData.append('partida_id', selectedPartidaForUpload.id);
// Agregar el ID del pedimento (por si acaso)
formData.append('pedimento_id', id);
// Agregar archivos al FormData
uploadFiles.forEach((file) => {
formData.append('files', file);
});
showMessage(`Subiendo ${uploadFiles.length} archivo(s) a la partida ${selectedPartidaForUpload.numero_partida}...`, 'info');
// const result = await postFormDataWithAuth(`${API_URL}/record/documents/bulk-upload/`, formData);
// showMessage(
// `${result.uploaded_count || uploadFiles.length} archivo(s) subido(s) exitosamente a la partida ${selectedPartidaForUpload.numero_partida}`,
// 'success'
// );
// Recargar las partidas para mostrar los nuevos documentos
fetchPedimentoPartidas(partidasPage, partidasPageSize, partidasFilters)
.then((data) => {
setPartidas(data.results || []);
setPartidasCount(data.count || 0);
})
.catch(err => {
console.error('Error recargando partidas:', err);
});
// Limpiar y cerrar
setSelectedPartidaForUpload(null);
setUploadFiles([]);
setUploadModalOpen(false)
}catch (error){
showMessage(`Error durante la subida: ${error.message}`, 'error');
} finally {
setUploadingToPartida(null);
}
};
// Función para manejar la selección de archivos
const handleUploadFileSelect = (event, fileExtension, seccion) => {
if (!event.target.files) return; // Evitar errores si es null
try{
const selectedFiles = Array.from(event.target.files).filter(file =>
file?.name?.toLowerCase().endsWith(fileExtension)
);
if (selectedFiles.length < event.target.files.length) {
showMessage(`Solo se aceptan archivos ${fileExtension}`, 'warning');
return;
}
const filesWithSection = selectedFiles.map((file) => {
const originalName = file.name;
const extension = originalName.split('.').pop();
const nameWithoutExtension = originalName.replace(`.${extension}`, '')
const newFileName = `${nameWithoutExtension}.${extension}.${seccion}`;
// Crear nuevo objeto File con el nombre modificado
const renamedFile = new File(
[file], // Los datos del archivo
newFileName, // Nuevo nombre
{
type: file.type,
lastModified: file.lastModified
}
);
return {
file: renamedFile,
originalName: originalName,
seccion: seccion
};
});
// setUploadFiles(prevFiles => [
// ...prevFiles,
// ...filesWithSection
// ]);
setUploadFiles(prevFiles => {
// Creamos un nuevo array reemplazando archivos existentes con misma extensión y sección
const updatedFiles = [...prevFiles];
filesWithSection.forEach(newFileObj => {
const index = updatedFiles.findIndex(f =>
f.seccion === newFileObj.seccion
);
if (index !== -1) {
// Reemplazar archivo existente
updatedFiles[index] = newFileObj;
} else {
// Agregar archivo nuevo
updatedFiles.push(newFileObj);
}
});
return updatedFiles;
});
// Limpiar input para permitir re-subida del mismo archivo
}catch(error){
showMessage(`Error durante la subida: ${error.message}`, 'error');
}finally{
// event.target.value = null;
}
};
// Función para manejar la carga de documentos a un cove
const handleUploadToCOVE = (cove) => {
cove.tab = 'cove';
cove.numero = cove.numero_cove;
// console.log(cove);
setSelectedDocumentForUpload(cove);
setUploadFiles([]);
setUploadModalOpen(true);
};
// Función para manejar la carga de documentos a un Edocument
const handleUploadToEdoc = (edoc) => {
edoc.tab = 'edoc';
edoc.numero = edoc.numero_edocument;
// console.log(edoc);
setSelectedDocumentForUpload(edoc);
setUploadFiles([]);
setUploadModalOpen(true);
};
// Función para manejar la carga de documentos a un Edocument
const handleUploadFilesToDocumento = async (tab) => {
console.log(tab);
if (!selectedDocumentForUpload || uploadFiles.length === 0) {
showMessage('Por favor selecciona al menos un archivo', 'warning');
return;
}
setUploadingToDocumento(selectedDocumentForUpload.id);
try{
const formData = new FormData();
// Agregar el ID del pedimento (por si acaso)
formData.append('pedimento_id', selectedDocumentForUpload.pedimento);
// Agregar el ID de la partida
formData.append('expediente_vu_id', selectedDocumentForUpload.id);
// Agregar archivos al FormData
// uploadFiles.forEach((file, seccion ) => {
// formData.append('files', file);
// formData.append('seccion', seccion);
// });
uploadFiles.forEach((fileObj, index) => {
formData.append('files', fileObj.file); // Mismo nombre para todos los archivos
// Si necesitas enviar secciones para cada archivo, puedes hacerlo así:
formData.append(`secciones[${index}]`, fileObj.seccion);
});
if (tab === 'partida'){
formData.append('tab_seccion', 'partida');
formData.append('numero', selectedDocumentForUpload.numero_partida);
showMessage(`Subiendo ${uploadFiles.length} archivo(s) a la partida ${selectedDocumentForUpload.numero_partida}...`, 'info');
}else if(tab === 'cove'){
formData.append('tab_seccion', 'cove');
formData.append('numero', selectedDocumentForUpload.numero_cove);
showMessage(`Subiendo ${uploadFiles.length} archivo(s) a la partida ${selectedDocumentForUpload.numero_cove}...`, 'info');
}else if(tab === 'edoc'){
formData.append('tab_seccion', 'edoc');
formData.append('numero', selectedDocumentForUpload.numero_edocument);
showMessage(`Subiendo ${uploadFiles.length} archivo(s) a la partida ${selectedDocumentForUpload.numero_edocument}...`, 'info');
}else{
showMessage('Carga de archivos a seccion desconocida', 'error');
return;
}
const result = await postFormDataWithAuth(`${API_URL}/record/documents/bulk-upload-vu/`, formData);
if(!result.ok){
const errorData = await result.json();
throw new Error(errorData.error || 'Error desconocido');
}
showMessage(
`${result.uploaded_count || uploadFiles.length} archivo(s) subido(s) exitosamente a ${tab}`,
'success'
);
// // Recargar las partidas para mostrar los nuevos documentos
// fetchPedimentoPartidas(partidasPage, partidasPageSize, partidasFilters)
// .then((data) => {
// setPartidas(data.results || []);
// setPartidasCount(data.count || 0);
// })
// .catch(err => {
// console.error('Error recargando partidas:', err);
// });
}catch (error){
showMessage(`Error durante la subida: ${error.message}`, 'error');
} finally {
// Limpiar y cerrar
setSelectedDocumentForUpload(null);
setUploadFiles([]);
setUploadModalOpen(false)
setUploadingToDocumento(null);
}
};
const isPartida = selectedDocumentForUpload?.tab === 'partida';
const isCove = selectedDocumentForUpload?.tab === 'cove';
const isEdoc = selectedDocumentForUpload?.tab === 'edoc';
// Estados para documentos de errores VU
const [errorDocuments, setErrorDocuments] = useState([]);
const [errorDocsCount, setErrorDocsCount] = useState(0);
const [errorDocsLoading, setErrorDocsLoading] = useState(true);
const [errorDocsError, setErrorDocsError] = useState('');
const [errorDocsPage, setErrorDocsPage] = useState(1);
const [errorDocsPageSize, setErrorDocsPageSize] = useState(10);
const [selectedErrorDocuments, setSelectedErrorDocuments] = useState([]);
const [isSelectAllErrorDocs, setIsSelectAllErrorDocs] = useState(false);
const [downloadingAllErrors, setDownloadingAllErrors] = useState(false);
// Filtros para errores VU
const [errorFilters, setErrorFilters] = useState({
name: '',
document_type: '',
extension: '',
date: '',
created_at__gte: '',
created_at__lte: '',
fuente: '',
pedimento_numero: '',
tipo_error: ''
});
// Estado para mostrar filtros de errores
const [showErrorFilters, setShowErrorFilters] = useState(false);
// Efecto para cargar documentos de errores VU
useEffect(() => {
// Función para obtener documentos de errores VU usando el endpoint específico
const fetchErrorDocuments = async (pedimentoId,page = 1, pageSize = 10, filters = {}) => {
try {
if (!id || !pedimento?.pedimento_app) return;
// Construir parámetros de filtros
const params = new URLSearchParams({
pedimento: pedimento.pedimento_app, // Usar pedimento_app como requiere el endpoint
pedimentoId: pedimentoId,
// document_type_id: [14,16,18,20,22,24,26], IDs de tipos de documentos de error VU
page: page,
page_size: pageSize
});
// Solo agregar filtros que tengan valores
Object.entries(filters).forEach(([key, value]) => {
if (value && value.toString().trim() !== '') {
params.append(key, value);
}
});
// Usar el endpoint específico para documentos de error VU
const response = await fetchWithAuth(
`${API_URL}/record/documents/vu-documentos-errores/?${params}`
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `Error ${response.status} al obtener documentos de error VU`);
}
const data = await response.json();
// El endpoint devuelve un array directo, no un objeto paginado
// Aplicamos paginación manualmente en el frontend
if (Array.isArray(data)) {
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedResults = data.slice(startIndex, endIndex);
return {
results: paginatedResults,
count: data.length,
next: endIndex < data.length ? page + 1 : null,
previous: page > 1 ? page - 1 : null
};
}
// Si por alguna razón devuelve un objeto paginado
return data;
} catch (error) {
console.error('Error fetching error documents:', error);
throw error;
}
};
if (!id || !pedimento || activeTab !== 'errores') return;
console.log('Fetching error documents for pedimento:', pedimento.pedimento_app);
setErrorDocsLoading(true);
setErrorDocsError('');
// Construir parámetros de filtros
const apiFilters = {};
if (errorFilters.name) apiFilters.archivo__icontains = errorFilters.name;
if (errorFilters.extension) apiFilters.extension = errorFilters.extension;
if (errorFilters.date) apiFilters.created_at__date = errorFilters.date;
if (errorFilters.created_at__gte) apiFilters.created_at__gte = errorFilters.created_at__gte;
if (errorFilters.created_at__lte) apiFilters.created_at__lte = errorFilters.created_at__lte;
if (errorFilters.fuente) apiFilters.fuente = errorFilters.fuente;
if (errorFilters.tipo_error) {
apiFilters.tipo_error = errorFilters.tipo_error;
}else{
apiFilters.tipo_error = [14,16,18,20,22,24,26];
}
// NOTA: No necesitamos document_type porque el endpoint ya filtra por documentos de error VU
// Tampoco necesitamos pedimento_numero porque ya estamos filtrando por pedimento
fetchErrorDocuments(id,errorDocsPage, errorDocsPageSize, apiFilters)
.then((data) => {
console.log('Error documents data:', data);
setErrorDocuments(data.results || []);
setErrorDocsCount(data.count || 0);
setErrorDocsLoading(false);
})
.catch(err => {
console.error('Error fetching error documents:', err);
if (err.message === 'SESSION_EXPIRED') {
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
} else {
setErrorDocsError(err.message || 'Error al cargar documentos de error VU');
}
setErrorDocsLoading(false);
});
}, [id, activeTab, errorDocsPage, errorDocsPageSize, errorFilters, showMessage, pedimento]);
// Resetear página cuando cambien los filtros de errores
useEffect(() => {
setErrorDocsPage(1);
}, [errorFilters]);
// Efecto para actualizar isSelectAllErrorDocs cuando cambia la selección
useEffect(() => {
if (errorDocuments.length > 0) {
const allSelected = errorDocuments.every(doc => selectedErrorDocuments.includes(doc.id));
setIsSelectAllErrorDocs(allSelected && selectedErrorDocuments.length > 0);
}
}, [selectedErrorDocuments, errorDocuments]);
// Efecto para limpiar selección cuando cambia de página
useEffect(() => {
setSelectedErrorDocuments([]);
setIsSelectAllErrorDocs(false);
}, [errorDocsPage]);
// Funciones para manejo de selección múltiple de documentos de error
const handleSelectErrorDocument = (documentId) => {
const isSelected = selectedErrorDocuments.includes(documentId);
if (isSelected) {
setSelectedErrorDocuments(prev => prev.filter(id => id !== documentId));
} else {
setSelectedErrorDocuments(prev => [...prev, documentId]);
}
};
const handleSelectAllErrorDocuments = () => {
if (isSelectAllErrorDocs) {
setSelectedErrorDocuments([]);
setIsSelectAllErrorDocs(false);
} else {
const allDocumentIds = errorDocuments.map(doc => doc.id);
setSelectedErrorDocuments(allDocumentIds);
setIsSelectAllErrorDocs(true);
}
};
// Función para eliminar documentos de error seleccionados
const handleDeleteSelectedErrorDocuments = async () => {
if (selectedErrorDocuments.length === 0) {
showMessage('No hay documentos seleccionados para eliminar', 'warning');
return;
}
try {
showMessage(`Eliminando ${selectedErrorDocuments.length} documento(s) de error...`, 'info');
const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-delete/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: selectedErrorDocuments
})
});
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 || selectedErrorDocuments.length} documento(s) de error eliminado(s) exitosamente`,
'success'
);
setSelectedErrorDocuments([]);
setIsSelectAllErrorDocs(false);
// Forzar recarga de documentos
const currentPage = errorDocsPage;
setErrorDocsPage(0);
setTimeout(() => setErrorDocsPage(currentPage), 100);
} catch (error) {
console.error('Error durante la eliminación masiva de documentos de error:', error);
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
}
};
// Función para descargar todos los documentos de error
const downloadAllErrors = async () => {
setDownloadingAllErrors(true);
try {
const allErrorDocIds = errorDocuments.map(doc => doc.id);
await handleBulkDownload(allErrorDocIds);
} catch (error) {
console.error('Error downloading all error documents:', error);
showMessage('Error al descargar todos los documentos de error', 'error');
} finally {
setDownloadingAllErrors(false);
}
};
//----------------------------------------//
// Estados de carga
if (loading) return (
<div className="flex items-center justify-center min-h-screen p-4 bg-gradient-to-br from-blue-50 via-white to-blue-50 lg:p-6">
<div className="text-center">
<div className="w-32 h-32 mx-auto mb-4 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<p className="text-lg text-gray-600">Cargando detalle del pedimento...</p>
</div>
</div>
);
if (error) return (
<div className="flex items-center justify-center min-h-screen p-4 bg-gradient-to-br from-blue-50 via-white to-blue-50 lg:p-6">
<div className="text-center">
<div className="flex items-center justify-center w-16 h-16 p-4 mx-auto 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>
<h2 className="mb-2 text-xl font-semibold text-gray-900">Error al cargar</h2>
<p className="text-gray-600">{error}</p>
<Link to="/expedientes" className="inline-flex items-center mt-4 text-blue-600 hover:text-blue-800">
Volver a expedientes
</Link>
</div>
</div>
);
return (
<div className="min-h-screen p-4 bg-gradient-to-br from-blue-50 via-white to-blue-50 lg:p-6">
<div className="mx-auto max-w-7xl">
{/* Header */}
<div className="mb-6 border shadow-xl opacity-0 bg-white/80 backdrop-blur-xl rounded-2xl border-blue-100/50 animate-fadein-slideup"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
<div className="p-6">
<Link
to="/expedientes"
className="inline-flex items-center mb-2 text-blue-600 transition-colors duration-200 hover:text-blue-800"
>
<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="M15 19l-7-7 7-7"></path>
</svg>
<span className="font-medium">Volver a la lista</span>
</Link>
<h1 className="mb-1 text-2xl font-bold text-gray-900 lg:text-3xl">
Detalle de Pedimento
</h1>
<p className="text-gray-600">
Información completa del pedimento y documentos asociados
</p>
{docsCount > 0 && (
<div className="mt-2">
<span className="inline-flex items-center px-3 py-1 text-sm font-medium text-blue-800 bg-blue-100 rounded-full">
📄 {docsCount} documentos
</span>
</div>
)}
</div>
</div>
{/* Información del Pedimento */}
{pedimento && (
<div className="mb-6 border shadow-xl opacity-0 bg-white/80 backdrop-blur-xl rounded-2xl border-blue-100/50 animate-fadein-slideup"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
<div className="px-6 py-4 border-b border-gray-200/50">
<div className="flex items-center gap-3">
<div className="p-2 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl">
<svg className="w-5 h-5 text-white" 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>
</div>
<h2 className="text-xl font-bold text-gray-900">Información General</h2>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
<div className="p-4 border shadow-sm bg-gradient-to-br from-blue-50 to-blue-100/50 rounded-xl border-blue-200/50">
<dt className="flex items-center mb-2 text-sm font-semibold text-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="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
Pedimento
</dt>
<dd className="text-xl font-bold text-gray-900">{pedimento.pedimento_app}</dd>
</div>
<div className="p-4 border shadow-sm bg-gradient-to-br from-green-50 to-green-100/50 rounded-xl border-green-200/50">
<dt className="flex items-center mb-2 text-sm font-semibold text-green-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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Contribuyente
</dt>
<dd className="text-xl font-bold text-gray-900">{pedimento.contribuyente}</dd>
</div>
<div className="p-4 border shadow-sm bg-gradient-to-br from-purple-50 to-purple-100/50 rounded-xl border-purple-200/50">
<dt className="flex items-center mb-2 text-sm font-semibold text-purple-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="M8 7V3a2 2 0 012-2h4a2 2 0 012 2v4m-6 0h6M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V9a2 2 0 00-2-2h-2" />
</svg>
Fecha de Pago
</dt>
<dd className="text-xl font-bold text-gray-900">{pedimento.fecha_pago}</dd>
</div>
</div>
</div>
</div>
)}
{/* Sistema de pestañas */}
<div className="border shadow-xl opacity-0 bg-white/80 backdrop-blur-xl rounded-2xl border-blue-100/50 animate-fadein-slideup"
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards' }}>
{/* Navegación de pestañas */}
<div className="border-b border-gray-200/50 bg-gradient-to-r from-gray-50 to-blue-50/30">
<nav className="flex px-3 space-x-2 overflow-x-auto sm:space-x-4 lg:space-x-8 sm:px-6 scrollbar-hide" aria-label="Pestañas">
{/* Nueva Pestaña Pedimento T2025-10-152*/}
<button
onClick={() => handleTabChange('pedimento')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'pedimento'
? 'border-blue-600 text-blue-600 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" 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="hidden sm:inline">Pedimento</span>
<span className="sm:hidden">PedDocs</span>
{activeTab === 'pedimento' && peddocsCount > 0 && (
<span className="bg-blue-100 text-blue-600 text-xs font-semibold px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full">
{peddocsCount}
</span>
)}
</div>
</button>
{/* Pestaña Partidas */}
<button
onClick={() => handleTabChange('partidas')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'partidas'
? 'border-blue-600 text-blue-600 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<span>Partidas</span>
{activeTab === 'partidas' && partidasCount > 0 && (
<span className="bg-purple-100 text-purple-600 text-xs font-semibold px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full">
{partidasCount}
</span>
)}
</div>
</button>
{/* Pestaña Coves */}
<button
onClick={() => handleTabChange('coves')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'coves'
? 'border-blue-600 text-blue-600 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
</svg>
<span className="hidden sm:inline">COVEs</span>
<span className="sm:hidden">COVEs</span>
</div>
</button>
{/* Pestaña Edocuments */}
<button
onClick={() => handleTabChange('edocs')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'edocs'
? 'border-blue-600 text-blue-600 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>EDocs</span>
</div>
</button>
{/* Pestaña Documentos */}
<button
onClick={() => handleTabChange('documentos')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'documentos'
? 'border-blue-600 text-blue-600 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" 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="hidden sm:inline">Documentos</span>
<span className="sm:hidden">Docs</span>
{activeTab === 'documentos' && docsCount > 0 && (
<span className="bg-blue-100 text-blue-600 text-xs font-semibold px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full">
{docsCount}
</span>
)}
</div>
</button>
{/* Pestaña Errores VU */}
<button
onClick={() => handleTabChange('errores')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'errores'
? 'border-red-600 text-red-600 bg-red-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<span className="hidden sm:inline">Errores VU</span>
<span className="sm:hidden">Errores</span>
{activeTab === 'errores' && errorDocsCount > 0 && (
<span className="bg-red-100 text-red-600 text-xs font-semibold px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full">
{errorDocsCount}
</span>
)}
</div>
</button>
{/* Pestaña Auditor */}
<button
onClick={() => handleTabChange('auditor')}
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
activeTab === 'auditor'
? 'border-blue-600 text-blue-600 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<div className="flex items-center space-x-1 sm:space-x-2">
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span className="hidden sm:inline">Auditor de Pedimento</span>
<span className="sm:hidden">Auditor</span>
</div>
</button>
</nav>
</div>
{/* Contenido de las pestañas */}
{activeTab === 'documentos' && (
<div className="p-6">
{/* Header de la sección */}
{/* <pre className="p-4 overflow-auto text-xs bg-gray-100">{JSON.stringify(documents, null, 2)}</pre> */}
<div className="mb-4 space-y-3 sm:mb-6 sm:space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<h3 className="text-lg font-semibold text-gray-900 sm:text-xl">
Documentos Generales
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 w-fit">
{docsCount} documentos
</span>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:gap-3">
<button
onClick={() => setShowUploadModal(true)}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg className="w-4 h-4 mr-1 sm: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>
<span className="hidden sm:inline">Subir Documentos</span>
<span className="sm:hidden">Subir</span>
</button>
{documents.length > 0 && (
<>
<button
onClick={downloadAll}
disabled={downloadingAll}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{downloadingAll ? (
<>
<svg className="w-4 h-4 mr-2 -ml-1 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" 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>
<span className="hidden sm:inline">Descargando...</span>
<span className="sm:hidden">Descargando...</span>
</>
) : (
<>
<svg className="w-4 h-4 mr-1 sm: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>
<span className="hidden sm:inline">Descargar Todos</span>
<span className="sm:hidden">Descargar</span>
</>
)}
</button>
<button
onClick={() => setShowFilters(!showFilters)}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg className="w-4 h-4 mr-1 sm:mr-2" 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>
<span className="hidden sm:inline">{showFilters ? 'Ocultar Filtros' : 'Mostrar Filtros'}</span>
<span className="sm:hidden">Filtros</span>
</button>
</>
)}
</div>
</div>
{/* Filtros expandibles */}
{showFilters && (
<div className="grid grid-cols-1 gap-4 p-4 border rounded-lg md:grid-cols-2 lg:grid-cols-4 bg-gray-50">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Buscar por nombre
</label>
<input
type="text"
value={filters.name}
onChange={(e) => setFilters(prev => ({ ...prev, name: e.target.value }))}
placeholder="Nombre del archivo..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
{/* <div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Tipo de documento
</label>
<select
value={filters.document_type}
onChange={(e) => setFilters(prev => ({ ...prev, document_type: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos los tipos</option>
<option value="1">Partida</option>
<option value="2">Pedimento Completo</option>
<option value="3">Remesa</option>
<option value="4">Acuse EDocument</option>
<option value="5">EDocument</option>
<option value="7">Acuse Cove</option>
<option value="8">Cove</option>
<option value="9">Documento de digitalización</option>
<option value="10">Error</option>
</select>
</div> */}
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Extensión
</label>
<select
value={filters.extension}
onChange={(e) => setFilters(prev => ({ ...prev, extension: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todas las extensiones</option>
<option value="pdf">PDF</option>
<option value="xml">XML</option>
<option value="jpg">JPG</option>
<option value="png">PNG</option>
<option value="doc">DOC</option>
<option value="docx">DOCX</option>
<option value="xls">XLS</option>
<option value="xlsx">XLSX</option>
<option value="txt">TXT</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fuente
</label>
<select
value={filters.fuente || ''}
onChange={(e) => setFilters(prev => ({ ...prev, fuente: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todas las fuentes</option>
<option value="1">Manual</option>
<option value="2">VU</option>
<option value="3">Importación</option>
<option value="4">Sistema</option>
</select>
</div>
{/* <div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Número de pedimento
</label>
<input
type="text"
value={filters.pedimento_numero || ''}
onChange={(e) => setFilters(prev => ({ ...prev, pedimento_numero: e.target.value }))}
placeholder="Número de pedimento..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div> */}
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha de creación
</label>
<input
type="date"
value={filters.date}
onChange={(e) => setFilters(prev => ({ ...prev, date: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div className="flex items-end">
<button
onClick={() => setFilters({
name: '',
document_type: '',
extension: '',
date: '',
fuente: '',
pedimento_numero: ''
})}
className="w-full px-3 py-2 text-sm font-medium text-gray-600 transition-colors bg-gray-100 rounded-md hover:bg-gray-200"
>
Limpiar filtros
</button>
</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([]);
setIsSelectAllDocs(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={handleDeleteSelectedDocuments}
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>
)}
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<span className="ml-2 text-gray-600">Cargando documentos...</span>
</div>
) : error ? (
<div className="py-12 text-center">
<div className="mb-2 text-red-600">
<svg className="w-12 h-12 mx-auto" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="mb-1 text-lg font-medium text-gray-900">Error al cargar documentos</h3>
<p className="text-gray-600">{error}</p>
</div>
) : documents.length === 0 ? (
<div className="py-12 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay documentos</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron documentos para este pedimento.
</p>
</div>
) : (
<div className="space-y-4">
{/* Tabla de documentos */}
<div className="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<input
type="checkbox"
checked={isSelectAllDocs}
onChange={handleSelectAllDocuments}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('archivo')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Documento</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('document_type')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Tipo</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('extension')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Extensión</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('size')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Tamaño</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fuente
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('created_at')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Fecha de Creación</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Acciones</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{documents.map((doc, index) => (
<tr key={`${doc.id}-${index}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedDocuments.includes(doc.id)}
onChange={() => handleSelectDocument(doc.id)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10">
<div className="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-lg">
<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="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>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900" title={doc.archivo}>
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
</div>
<div className="text-sm text-gray-500">
Pedimento: {doc.pedimento_numero || 'N/A'}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{getDocumentTypeName(doc.document_type)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{doc.extension ? doc.extension.toUpperCase() : 'N/A'}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-900 whitespace-nowrap">
{formatFileSize(doc.size)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{/* {getFuenteName(doc.fuente)} */}
{doc.fuente_nombre}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{formatDate(doc.created_at)}
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<div className="flex items-center space-x-2">
<button
onClick={() => previewDocument(doc)}
className="p-1 text-blue-600 rounded hover:text-blue-900"
title="Vista previa"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button
onClick={() => downloadDocument(doc)}
className="p-1 text-green-600 rounded hover:text-green-900"
title="Descargar"
>
<svg className="w-4 h-4" 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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para documentos */}
{docsCount > 0 && (
<div className="flex flex-col gap-4 mt-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span className="text-sm text-gray-700">
<span className="hidden sm:inline">Mostrando </span>
<span className="font-medium">{((page - 1) * pageSize) + 1}</span>-
<span className="font-medium">{Math.min(page * pageSize, docsCount)}</span>
<span className="hidden sm:inline">de </span>
<span className="sm:hidden">/</span>
<span className="font-medium">{docsCount}</span>
<span className="hidden sm:inline"> documentos</span>
</span>
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setPage(1);
}}
className="w-full text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 sm:w-auto"
>
<option value={10}>10 por página</option>
<option value={25}>25 por página</option>
<option value={50}>50 por página</option>
<option value={100}>100 por página</option>
</select>
</div>
<div className="flex items-center justify-center space-x-1 sm:justify-end sm:space-x-2">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<span className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300">
Página {page} de {Math.ceil(docsCount / pageSize)}
</span>
<button
onClick={() => setPage(Math.min(Math.ceil(docsCount / pageSize), page + 1))}
disabled={page >= Math.ceil(docsCount / pageSize)}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
{activeTab === 'pedimento' && (
<div className="p-6">
{/* Header de la sección */}
<div className="mb-4 space-y-3 sm:mb-6 sm:space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<h3 className="text-lg font-semibold text-gray-900 sm:text-xl">
Pedimento Completo VU
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 w-fit">
{peddocsCount} documentos
</span>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:gap-3">
<button
onClick={() => setShowUploadModal(true)}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg className="w-4 h-4 mr-1 sm: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>
<span className="hidden sm:inline">Subir Documentos</span>
<span className="sm:hidden">Subir</span>
</button>
{peddocuments.length > 0 && (
<>
<button
onClick={downloadAllPedimento}
disabled={downloadingAllPedimento}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{downloadingAllPedimento ? (
<>
<svg className="w-4 h-4 mr-2 -ml-1 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" 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>
<span className="hidden sm:inline">Descargando...</span>
<span className="sm:hidden">Descargando...</span>
</>
) : (
<>
<svg className="w-4 h-4 mr-1 sm: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>
<span className="hidden sm:inline">Descargar Todos</span>
<span className="sm:hidden">Descargar</span>
</>
)}
</button>
<button
onClick={() => setShowFiltersPedimento(!showFiltersPedimento)}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg className="w-4 h-4 mr-1 sm:mr-2" 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>
<span className="hidden sm:inline">{showFiltersPedimento ? 'Ocultar Filtros' : 'Mostrar Filtros'}</span>
<span className="sm:hidden">Filtros</span>
</button>
</>
)}
</div>
</div>
{/* Filtros expandibles */}
{showFiltersPedimento && (
<div className="grid grid-cols-1 gap-4 p-4 border rounded-lg md:grid-cols-2 lg:grid-cols-4 bg-gray-50">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Buscar por nombre
</label>
<input
type="text"
value={pedimentoFilters.name}
onChange={(e) => setPedimentoFilters(prev => ({ ...prev, name: e.target.value }))}
placeholder="Nombre del archivo..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Tipo de documento
</label>
<select
value={pedimentoFilters.document_type}
onChange={(e) => setPedimentoFilters(prev => ({ ...prev, document_type: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos los tipos</option>
<option value="2">Pedimento Completo</option>
<option value="3">Remesa</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Extensión
</label>
<select
value={pedimentoFilters.extension}
onChange={(e) => setPedimentoFilters(prev => ({ ...prev, extension: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todas las extensiones</option>
<option value="pdf">PDF</option>
<option value="xml">XML</option>
<option value="jpg">JPG</option>
<option value="png">PNG</option>
<option value="doc">DOC</option>
<option value="docx">DOCX</option>
<option value="xls">XLS</option>
<option value="xlsx">XLSX</option>
<option value="txt">TXT</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fuente
</label>
<select
value={pedimentoFilters.fuente || ''}
onChange={(e) => setPedimentoFilters(prev => ({ ...prev, fuente: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todas las fuentes</option>
<option value="1">Manual</option>
<option value="2">VU</option>
<option value="3">Importación</option>
<option value="4">Sistema</option>
</select>
</div>
{/* <div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Número de pedimento
</label>
<input
type="text"
value={pedimentoFilters.pedimento_numero || ''}
onChange={(e) => setPedimentoFilters(prev => ({ ...prev, pedimento_numero: e.target.value }))}
placeholder="Número de pedimento..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div> */}
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha de creación
</label>
<input
type="date"
value={pedimentoFilters.date}
onChange={(e) => setPedimentoFilters(prev => ({ ...prev, date: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div className="flex items-end">
<button
onClick={() => setPedimentoFilters({
name: '',
document_type: '',
extension: '',
date: '',
fuente: '',
pedimento_numero: ''
})}
className="w-full px-3 py-2 text-sm font-medium text-gray-600 transition-colors bg-gray-100 rounded-md hover:bg-gray-200"
>
Limpiar filtros
</button>
</div>
</div>
)}
</div>
{/* Área de acciones para documentos seleccionados */}
{selectedPedimentos.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">
{selectedPedimentos.length} documento{selectedPedimentos.length !== 1 ? 's' : ''} seleccionado{selectedPedimentos.length !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-gray-600">Selecciona una acción para continuar</p>
</div>
</div>
<button
onClick={() => {
setSelectedPedimentos([]);
setIsSelectAllPedimentos(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={handleDeleteSelectedPedimentosVU}
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>
)}
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<span className="ml-2 text-gray-600">Cargando documentos...</span>
</div>
) : error ? (
<div className="py-12 text-center">
<div className="mb-2 text-red-600">
<svg className="w-12 h-12 mx-auto" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="mb-1 text-lg font-medium text-gray-900">Error al cargar documentos</h3>
<p className="text-gray-600">{error}</p>
</div>
) : peddocuments.length === 0 ? (
<div className="py-12 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay documentos VU</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron documentos de VU.
</p>
</div>
) : (
<div className="space-y-4">
{/* Tabla de documentos */}
<div className="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<input
type="checkbox"
checked={allPedimentosSelected}
onChange={handleSelectAllPedimentos}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('archivo')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Documento</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('document_type')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Tipo</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('extension')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Extensión</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('size')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Tamaño</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fuente
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<button
onClick={() => handleSort('created_at')}
className="inline-flex items-center space-x-1 group hover:text-gray-900"
>
<span>Fecha de Creación</span>
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</button>
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Acciones</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{peddocuments.map((doc, index) => (
<tr key={`${doc.id}-${index}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedPedimentos.includes(doc.id)}
onChange={() => handleSelectPedimento(doc.id)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10">
<div className="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-lg">
<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="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>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900" title={doc.archivo}>
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
</div>
<div className="text-sm text-gray-500">
Pedimento: {doc.pedimento_numero || 'N/A'}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{getDocumentTypeName(doc.document_type)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{doc.extension ? doc.extension.toUpperCase() : 'N/A'}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-900 whitespace-nowrap">
{formatFileSize(doc.size)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{getFuenteName(doc.fuente)}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{formatDate(doc.created_at)}
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<div className="flex items-center space-x-2">
<button
onClick={() => previewDocument(doc)}
className="p-1 text-blue-600 rounded hover:text-blue-900"
title="Vista previa"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button
onClick={() => downloadDocument(doc)}
className="p-1 text-green-600 rounded hover:text-green-900"
title="Descargar"
>
<svg className="w-4 h-4" 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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para documentos */}
{peddocsCount > 0 && (
<div className="flex flex-col gap-4 mt-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span className="text-sm text-gray-700">
<span className="hidden sm:inline">Mostrando </span>
<span className="font-medium">{((pedimentoPage - 1) * pedimentoPageSize) + 1}</span>-
<span className="font-medium">{Math.min(pedimentoPage * pedimentoPageSize, peddocsCount)}</span>
<span className="hidden sm:inline">de </span>
<span className="sm:hidden">/</span>
<span className="font-medium">{peddocsCount}</span>
<span className="hidden sm:inline"> documentos</span>
</span>
<select
value={pedimentoPageSize}
onChange={(e) => {
setPedimentoPageSize(Number(e.target.value));
setPedimentoPage(1);
}}
className="w-full text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 sm:w-auto"
>
<option value={10}>10 por página</option>
<option value={25}>25 por página</option>
<option value={50}>50 por página</option>
<option value={100}>100 por página</option>
</select>
</div>
<div className="flex items-center justify-center space-x-1 sm:justify-end sm:space-x-2">
<button
onClick={() => setPedimentoPage(Math.max(1, pedimentoPage - 1))}
disabled={pedimentoPage === 1}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<span className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300">
Página {pedimentoPage} de {Math.ceil(peddocsCount / pedimentoPageSize)}
</span>
<button
onClick={() => setPedimentoPage(Math.min(Math.ceil(peddocsCount / pedimentoPageSize), pedimentoPage + 1))}
disabled={pedimentoPage >= Math.ceil(peddocsCount / pedimentoPageSize)}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
{activeTab === 'partidas' && (
<div className="p-6">
{/* <pre className="p-4 overflow-auto text-xs bg-gray-100">{JSON.stringify(partidas, null, 2)}</pre> */}
{/* Header de la sección */}
<div className="mb-4 space-y-3 sm:mb-6 sm:space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<h3 className="text-lg font-semibold text-gray-900 sm:text-xl">
Partidas VU del Pedimento
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 w-fit">
{partidasCount} partidas
</span>
</div>
{/* Botones de acción masiva */}
{/* <div className="flex items-center space-x-3">
{selectedPartidas.length > 0 && (
<button
onClick={() => handleBulkDownloadPartidas(selectedPartidas)}
disabled={downloadingPartidas}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{downloadingPartidas ? (
<>
<svg className="w-4 h-4 mr-3 -ml-1 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" 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>
Descargando...
</>
) : (
<>
<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>
Descargar seleccionadas ({selectedPartidas.length})
</>
)}
</button>
)}
</div> */}
</div>
{/* Filtros */}
<div className="grid grid-cols-1 gap-3 p-3 border rounded-lg sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-7 sm:gap-4 sm:p-4 bg-gray-50">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Número de partida
</label>
<input
type="text"
value={partidasFilters.numero_partida}
onChange={(e) => setPartidasFilters(prev => ({ ...prev, numero_partida: e.target.value }))}
placeholder="Número de partida..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Desde partida #
</label>
<input
type="number"
min="1"
max={partidasCount || 1}
value={partidasFilters.numero_partida__gte || ''}
onChange={(e) => setPartidasFilters(prev => ({ ...prev, numero_partida__gte: e.target.value }))}
placeholder="1"
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Hasta partida #
</label>
<input
type="number"
min="1"
max={partidasCount || 1}
value={partidasFilters.numero_partida__lte || ''}
onChange={(e) => setPartidasFilters(prev => ({ ...prev, numero_partida__lte: e.target.value }))}
placeholder={partidasCount?.toString() || "1"}
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Estado de descarga
</label>
<select
value={partidasFilters.descargado}
onChange={(e) => setPartidasFilters(prev => ({ ...prev, descargado: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos</option>
<option value="true">Descargado</option>
<option value="false">No descargado</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha desde
</label>
<input
type="date"
value={partidasFilters.created_at__gte || ''}
onChange={(e) => setPartidasFilters(prev => ({ ...prev, created_at__gte: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha hasta
</label>
<input
type="date"
value={partidasFilters.created_at__lte || ''}
onChange={(e) => setPartidasFilters(prev => ({ ...prev, created_at__lte: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div className="flex items-end">
<button
onClick={() => setPartidasFilters({
numero_partida: '',
descargado: '',
numero_partida__gte: '',
numero_partida__lte: '',
created_at__gte: '',
created_at__lte: ''
})}
className="w-full px-3 py-2 text-sm font-medium text-gray-600 transition-colors bg-gray-100 rounded-md hover:bg-gray-200"
>
Limpiar filtros
</button>
</div>
</div>
</div>
{/* Área de acciones para documentos seleccionados */}
{selectedPartidas.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">
{selectedPartidas.length} documento{selectedPartidas.length !== 1 ? 's' : ''} seleccionado{selectedPartidas.length !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-gray-600">Selecciona una acción para continuar</p>
</div>
</div>
<button
onClick={() => {
setSelectedPartidas([]);
}}
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={handleDeleteSelectedPartidaVU}
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>
)}
{partidasLoading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<span className="ml-2 text-gray-600">Cargando partidas...</span>
</div>
) : partidasError ? (
<div className="py-12 text-center">
<div className="mb-2 text-red-600">
<svg className="w-12 h-12 mx-auto" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="mb-1 text-lg font-medium text-gray-900">Error al cargar partidas</h3>
<p className="text-gray-600">{partidasError}</p>
</div>
) : !partidas || partidas.length === 0 ? (
<div className="py-12 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay partidas</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron partidas para este pedimento.
</p>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-end gap-3 mb-2">
{/* Botones de acción masiva */}
<div className="flex items-center justify-end gap-3 mb-2">
{selectedPartidas.length > 0 && (
<button
onClick={() => handleBulkDownloadPartidas(selectedPartidas)}
disabled={downloadingPartidas}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{downloadingPartidas ? (
<>
<svg className="w-4 h-4 mr-3 -ml-1 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" 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>
Descargando...
</>
) : (
<>
<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>
Descargar seleccionadas ({selectedPartidas.length})
</>
)}
</button>
)}
</div>
{/* Botón Descargar partidas y tabla de partidas */}
<div className="flex justify-end mb-2">
<button
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={async () => {
if (!partidas || partidas.length === 0) {
showMessage('No hay partidas para procesar', 'info');
return;
}
const pendientes = partidas.filter(p => !p.descargado);
if (pendientes.length === 0) {
showMessage('No hay partidas pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const partida of pendientes) {
try {
await handlePartidaRequest(partida);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} partida(s) procesadas exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} partida(s) no se pudieron procesar`, 'error');
}}
>
Descargar partidas
</button>
</div>
</div>
<div className="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="relative px-6 py-3">
<input
type="checkbox"
className="absolute w-4 h-4 -mt-2 text-blue-600 border-gray-300 rounded left-4 top-1/2 focus:ring-blue-500"
checked={allPartidasSelected}
onChange={handleSelectAllPartidas}
/>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
ID
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Número de Partida
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Estado de Descarga
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fecha de Creación
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Última Actualización
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-right text-gray-500 uppercase">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{(partidas || []).map((partida, index) => (
<tr key={`${partida.id}-${index}`} className="hover:bg-gray-50">
<td className="relative px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
className="absolute w-4 h-4 -mt-2 text-blue-600 border-gray-300 rounded left-4 top-1/2 focus:ring-blue-500"
checked={selectedPartidas.includes(partida.id)}
onChange={() => handleSelectPartida(partida.id)}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10">
<div className="flex items-center justify-center w-10 h-10 bg-purple-100 rounded-lg">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
#{partida.id}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{partida.numero_partida}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
partida.descargado
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{partida.descargado ? 'Descargado' : 'Pendiente'}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{partida.created_at ? formatDate(partida.created_at) : 'N/A'}
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{partida.updated_at ? formatDate(partida.updated_at) : 'N/A'}
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<div className="flex items-center justify-end space-x-2">
{/* Botón Petición (solo activo si está pendiente) */}
<button
onClick={() => handlePartidaRequest(partida)}
disabled={partida.descargado || processingPartida === partida.id}
className={`p-1 rounded transition-colors ${
partida.descargado || processingPartida === partida.id
? 'text-gray-400 cursor-not-allowed'
: 'text-purple-600 hover:text-purple-900'
}`}
title={
processingPartida === partida.id ? 'Procesando partida...' :
partida.descargado ? 'No disponible - Ya descargado' :
'Procesar petición'
}
>
{processingPartida === partida.id ? (
<svg className="w-4 h-4 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>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 4H6a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-2m-4-1v8m0 0l3-3m-3 3L9 8m-5 5h2.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H20" />
</svg>
)}
</button>
{/* Botón para cargar documentos a la partida */}
<button
onClick={() => handleUploadToPartida(partida)}
className="p-1 text-green-600 rounded hover:text-green-900"
title="Cargar documentos a esta partida"
>
<FileUp size={16} strokeWidth={2} />
</button>
{/* Botón Documentos VU Partidas */}
<button
onClick={() => handleShowPartidaDocuments(partida)}
className={`p-1 rounded transition-colors ${
!partida.documentos || partida.documentos.length === 0
? 'text-gray-400 cursor-not-allowed'
: 'text-blue-600 rounded hover:text-blue-900'
}`}
disabled={!partida.documentos || partida.documentos.length === 0}
title={!partida.documentos || partida.documentos.length === 0
? 'No hay documentos disponibles'
: partida.documentos && partida.documentos.length > 0
? `Ver ${partida.documentos.length} documento(s) VU`
: 'Documentos VU COVE'
}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11v5m0 0 2-2m-2 2-2-2M3 6v1a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1Zm2 2v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8H5Z"/>
</svg>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para partidas */}
{partidasCount > 0 && (
<div className="flex flex-col gap-4 mt-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span className="text-sm text-gray-700">
<span className="hidden sm:inline">Mostrando </span>
<span className="font-medium">{((partidasPage - 1) * partidasPageSize) + 1}</span>-
<span className="font-medium">{Math.min(partidasPage * partidasPageSize, partidasCount)}</span>
<span className="hidden sm:inline"> de </span>
<span className="sm:hidden">/</span>
<span className="font-medium">{partidasCount}</span>
<span className="hidden sm:inline"> partidas</span>
</span>
<select
value={partidasPageSize}
onChange={(e) => {
setPartidasPageSize(Number(e.target.value));
setPartidasPage(1);
}}
className="w-full text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 sm:w-auto"
>
<option value={10}>10 por página</option>
<option value={25}>25 por página</option>
<option value={50}>50 por página</option>
<option value={100}>100 por página</option>
</select>
</div>
<div className="flex items-center justify-center space-x-1 sm:justify-end sm:space-x-2">
<button
onClick={() => setPartidasPage(Math.max(1, partidasPage - 1))}
disabled={partidasPage === 1}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<span className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300">
Página {partidasPage} de {Math.ceil((partidasCount || 0) / (partidasPageSize || 10)) || 1}
</span>
<button
onClick={() => setPartidasPage(Math.min(Math.ceil((partidasCount || 0) / (partidasPageSize || 10)) || 1, partidasPage + 1))}
disabled={partidasPage >= (Math.ceil((partidasCount || 0) / (partidasPageSize || 10)) || 1)}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
{activeTab === 'coves' && (
<div className="p-6">
{/* <pre className="p-4 overflow-auto text-xs bg-gray-100">{JSON.stringify(coves, null, 2)}</pre> */}
{/* Header de la sección */}
<div className="mb-4 space-y-3 sm:mb-6 sm:space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<h3 className="text-lg font-semibold text-gray-900 sm:text-xl">
COVEs VU del Pedimento
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 w-fit">
{covesCount} COVEs
</span>
</div>
</div>
{/* Botones para descargar todos los AcuseCoves y COVEs */}
{coves.length > 0 && (
<div className="flex flex-col justify-end gap-2 mb-2 sm:flex-row">
<button
onClick={handleDownloadAllAcuseCoves}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Descargar todos los AcuseCoves
</button>
<button
onClick={handleDownloadAllCoves}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Descargar todos los COVEs
</button>
</div>
)}
{/* Filtros */}
<div className="grid grid-cols-1 gap-3 p-3 border rounded-lg sm:grid-cols-2 lg:grid-cols-4 sm:gap-4 sm:p-4 bg-gray-50">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Buscar por número COVE
</label>
<input
type="text"
value={covesFilters.numero_cove}
onChange={(e) => setCovesFilters(prev => ({ ...prev, numero_cove: e.target.value }))}
placeholder="Número COVE..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
COVE descargado
</label>
<select
value={covesFilters.cove_descargado}
onChange={(e) => setCovesFilters(prev => ({ ...prev, cove_descargado: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos</option>
<option value="true">Descargado</option>
<option value="false">No descargado</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Acuse descargado
</label>
<select
value={covesFilters.acuse_cove_descargado}
onChange={(e) => setCovesFilters(prev => ({ ...prev, acuse_cove_descargado: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos</option>
<option value="true">Descargado</option>
<option value="false">No descargado</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha desde
</label>
<input
type="date"
value={covesFilters.created_at__gte}
onChange={(e) => setCovesFilters(prev => ({ ...prev, created_at__gte: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
</div>
</div>
{/* Área de acciones para documentos seleccionados */}
{selectedCoves.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">
{selectedCoves.length} documento{selectedCoves.length !== 1 ? 's' : ''} seleccionado{selectedCoves.length !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-gray-600">Selecciona una acción para continuar</p>
</div>
</div>
<button
onClick={() => {
setSelectedCoves([]);
}}
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={handleDeleteSelectedCovesVU}
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>
)}
{covesLoading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-b-2 border-blue-600 rounded-full animate-spin"></div>
<span className="ml-2 text-gray-600">Cargando COVEs...</span>
</div>
) : covesError ? (
<div className="py-12 text-center">
<div className="mb-2 text-red-600">
<svg className="w-12 h-12 mx-auto" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="mb-1 text-lg font-medium text-gray-900">Error al cargar COVEs</h3>
<p className="text-gray-600">{covesError}</p>
</div>
) : coves.length === 0 ? (
<div className="py-12 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay COVEs</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron COVEs para este pedimento.
</p>
</div>
) : (
<div className="space-y-4">
{/* Tabla de COVEs */}
<div className="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<input
type="checkbox"
checked={allCovesSelected}
onChange={handleSelectAllCoves}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Número COVE
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fecha de Creación
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Estado COVE
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Estado Acuse
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Acciones</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{coves.map((cove, index) => (
<tr key={`${cove.id}-${index}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedCoves.includes(cove.id)}
onChange={() => handleSelectCove(cove.id)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10">
<div className="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-lg">
<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="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
</svg>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{cove.numero_cove}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{formatDate(cove.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
cove.cove_descargado
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{cove.cove_descargado ? 'Descargado' : 'Pendiente'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
cove.acuse_cove_descargado
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{cove.acuse_cove_descargado ? 'Descargado' : 'Pendiente'}
</span>
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<div className="flex items-center justify-end space-x-2">
{/* Botón COVE */}
<button
onClick={() => handleCoveProcess(cove)}
disabled={cove.cove_descargado || processingCove === cove.id}
className={`p-1 rounded transition-colors ${
cove.cove_descargado
? 'text-gray-400 cursor-not-allowed'
: processingCove === cove.id
? 'text-blue-400 cursor-not-allowed'
: 'text-blue-600 hover:text-blue-900'
}`}
title={cove.cove_descargado ? 'COVE ya descargado' : processingCove === cove.id ? 'Procesando COVE...' : 'Procesar COVE'}
>
{processingCove === cove.id ? (
<svg className="w-4 h-4 animate-spin" 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>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h8m-10-4a9 9 0 1118 0 9 9 0 01-18 0z" />
</svg>
)}
</button>
{/* Botón Acuse de COVE */}
<button
onClick={() => handleAcuseCoveProcess(cove)}
disabled={cove.acuse_cove_descargado || processingAcuseCove === cove.id}
className={`p-1 rounded transition-colors ${
cove.acuse_cove_descargado
? 'text-gray-400 cursor-not-allowed'
: processingAcuseCove === cove.id
? 'text-green-400 cursor-not-allowed'
: 'text-green-600 hover:text-green-900'
}`}
title={cove.acuse_cove_descargado ? 'Acuse de COVE ya descargado' : processingAcuseCove === cove.id ? 'Procesando Acuse de COVE...' : 'Procesar Acuse de COVE'}
>
{processingAcuseCove === cove.id ? (
<svg className="w-4 h-4 animate-spin" 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>
) : (
<svg className="w-4 h-4" 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>
)}
</button>
{/* Botón para cargar documentos al cove */}
<button
onClick={() => handleUploadToCOVE(cove)}
className="p-1 text-green-600 rounded hover:text-green-900"
title="Cargar documentos a este cove"
>
<FileUp size={16} strokeWidth={2} />
</button>
{/* Botón Documentos VU COVE */}
<button
onClick={() => handleShowCoveDocuments(cove)}
className={`p-1 rounded transition-colors ${
!cove.documentos || cove.documentos.length === 0
? 'text-gray-400 cursor-not-allowed'
: 'text-blue-600 rounded hover:text-blue-900'
}`}
disabled={!cove.documentos || cove.documentos.length === 0}
title={!cove.documentos || cove.documentos.length === 0
? 'No hay documentos disponibles'
: cove.documentos && cove.documentos.length > 0
? `Ver ${cove.documentos.length} documento(s) VU`
: 'Documentos VU COVE'
}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11v5m0 0 2-2m-2 2-2-2M3 6v1a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1Zm2 2v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8H5Z"/>
</svg>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para COVEs */}
{covesCount > 0 && (
<div className="flex flex-col gap-4 mt-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span className="text-sm text-gray-700">
<span className="hidden sm:inline">Mostrando </span>
<span className="font-medium">{((covesPage - 1) * covesPageSize) + 1}</span>-
<span className="font-medium">{Math.min(covesPage * covesPageSize, covesCount)}</span>
<span className="hidden sm:inline"> de </span>
<span className="sm:hidden">/</span>
<span className="font-medium">{covesCount}</span>
<span className="hidden sm:inline"> COVEs</span>
</span>
<select
value={covesPageSize}
onChange={(e) => {
setCovesPageSize(Number(e.target.value));
setCovesPage(1);
}}
className="w-full text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 sm:w-auto"
>
<option value={10}>10 por página</option>
<option value={25}>25 por página</option>
<option value={50}>50 por página</option>
<option value={100}>100 por página</option>
</select>
</div>
<div className="flex items-center justify-center space-x-1 sm:justify-end sm:space-x-2">
<button
onClick={() => setCovesPage(Math.max(1, covesPage - 1))}
disabled={covesPage === 1}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<span className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300">
Página {covesPage} de {Math.ceil(covesCount / covesPageSize)}
</span>
<button
onClick={() => setCovesPage(Math.min(Math.ceil(covesCount / covesPageSize), covesPage + 1))}
disabled={covesPage >= Math.ceil(covesCount / covesPageSize)}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
{activeTab === 'edocs' && (
<div className="p-6">
{/* <pre className="p-4 overflow-auto text-xs bg-gray-100">{JSON.stringify(edocs, null, 2)}</pre> */}
{/* Header de la sección */}
<div className="mb-6 space-y-4">
<div className="flex flex-col gap-2 mb-2 sm:flex-row sm:justify-end">
<button
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={async () => {
if (!edocs || edocs.length === 0) {
showMessage('No hay EDocs para procesar', 'info');
return;
}
const pendientes = edocs.filter(e => !e.edocument_descargado);
if (pendientes.length === 0) {
showMessage('No hay EDocs pendientes por procesar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const edoc of pendientes) {
try {
await handleEdocProcess(edoc);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} EDoc(s) procesados exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} EDoc(s) no se pudieron procesar`, 'error');
}}
>
Descargar todos los EDocs
</button>
<button
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors bg-green-600 border border-transparent rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
onClick={async () => {
if (!edocs || edocs.length === 0) {
showMessage('No hay acuses de EDocs para descargar', 'info');
return;
}
const pendientes = edocs.filter(e => !e.acuse_descargado);
if (pendientes.length === 0) {
showMessage('No hay acuses de EDocs pendientes por descargar', 'info');
return;
}
let successCount = 0;
let errorCount = 0;
for (const edoc of pendientes) {
try {
await handleAcuseEdocDownload(edoc);
successCount++;
} catch (error) {
errorCount++;
}
}
if (successCount > 0) showMessage(`${successCount} acuse(s) de EDoc descargados exitosamente`, 'success');
if (errorCount > 0) showMessage(`${errorCount} acuse(s) de EDoc no se pudieron descargar`, 'error');
}}
>
Descargar Todos los acuses
</button>
</div>
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center space-x-4">
<h3 className="text-lg font-semibold text-gray-900">
EDocs VU del Pedimento
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{edocsCount} EDocs
</span>
</div>
</div>
{/* Filtros */}
<div className="grid grid-cols-1 gap-4 p-4 border rounded-lg md:grid-cols-2 lg:grid-cols-4 bg-gray-50">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Buscar por número EDocs
</label>
<input
type="text"
value={edocsFilters.numero_edocument}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, numero_edocument: e.target.value }))}
placeholder="Número EDocs..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Clave
</label>
<input
type="text"
value={edocsFilters.clave}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, clave: e.target.value }))}
placeholder="Clave..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Descripción
</label>
<input
type="text"
value={edocsFilters.descripcion}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, descripcion: e.target.value }))}
placeholder="Descripción..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
EDocs descargado
</label>
<select
value={edocsFilters.edocument_descargado}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, edocument_descargado: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos</option>
<option value="true">Descargado</option>
<option value="false">No descargado</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Acuse descargado
</label>
<select
value={edocsFilters.acuse_descargado}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, acuse_descargado: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
>
<option value="">Todos</option>
<option value="true">Descargado</option>
<option value="false">No descargado</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha desde
</label>
<input
type="date"
value={edocsFilters.created_at__gte}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, created_at__gte: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha hasta
</label>
<input
type="date"
value={edocsFilters.created_at__lte}
onChange={(e) => setEdocsFilters(prev => ({ ...prev, created_at__lte: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
</div>
</div>
{/* Área de acciones para documentos seleccionados */}
{selectedEdocs.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">
{selectedEdocs.length} documento{selectedEdocs.length !== 1 ? 's' : ''} seleccionado{selectedEdocs.length !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-gray-600">Selecciona una acción para continuar</p>
</div>
</div>
<button
onClick={() => {
setSelectedEdocs([]);
}}
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={handleDeleteSelectedEdocsVU}
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>
)}
{edocsLoading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-b-2 border-green-600 rounded-full animate-spin"></div>
<span className="ml-2 text-gray-600">Cargando EDocs...</span>
</div>
) : edocsError ? (
<div className="py-12 text-center">
<div className="mb-2 text-red-600">
<svg className="w-12 h-12 mx-auto" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="mb-1 text-lg font-medium text-gray-900">Error al cargar EDocs</h3>
<p className="text-gray-600">{edocsError}</p>
</div>
) : edocs.length === 0 ? (
<div className="py-12 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay EDocs</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron documentos electrónicos para este pedimento.
</p>
</div>
) : (
<div className="space-y-4">
{/* Tabla de EDocs */}
<div className="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<input
type="checkbox"
checked={allEdocsSelected}
onChange={handleSelectAllEdocs}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Número EDocs
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Clave
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Descripción
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fecha de Creación
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Estado EDocs
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Estado Acuse
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Acciones</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{edocs.map((edoc, index) => (
<tr key={`${edoc.id}-${index}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedEdocs.includes(edoc.id)}
onChange={() => handleSelectEdoc(edoc.id)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10">
<div className="flex items-center justify-center w-10 h-10 bg-green-100 rounded-lg">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{edoc.numero_edocument}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{edoc.clave || 'N/A'}
</span>
</td>
<td className="max-w-xs px-6 py-4 text-sm text-gray-900 truncate whitespace-nowrap" title={edoc.descripcion}>
{edoc.descripcion || 'Sin descripción'}
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{formatDate(edoc.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
edoc.edocument_descargado
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{edoc.edocument_descargado ? 'Descargado' : 'Pendiente'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
edoc.acuse_descargado
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{edoc.acuse_descargado ? 'Descargado' : 'Pendiente'}
</span>
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<div className="flex items-center justify-end space-x-2">
{/* Botón EDoc */}
<button
onClick={() => handleEdocProcess(edoc)}
disabled={edoc.edocument_descargado || processingEdoc === edoc.id}
className={`p-1 rounded transition-colors ${
edoc.edocument_descargado
? 'text-gray-400 cursor-not-allowed'
: processingEdoc === edoc.id
? 'text-blue-400 cursor-not-allowed'
: 'text-blue-600 hover:text-blue-900'
}`}
title={edoc.edocument_descargado ? 'EDoc ya descargado' : processingEdoc === edoc.id ? 'Procesando EDoc...' : 'Procesar EDoc'}
>
{processingEdoc === edoc.id ? (
<svg className="w-4 h-4 animate-spin" 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>
) : (
<svg className="w-4 h-4" 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>
)}
</button>
{/* Botón Acuse de EDoc */}
<button
onClick={() => handleAcuseEdocProcess(edoc)}
disabled={edoc.acuse_descargado || processingAcuseEdoc === edoc.id}
className={`p-1 rounded transition-colors ${
edoc.acuse_descargado
? 'text-gray-400 cursor-not-allowed'
: processingAcuseEdoc === edoc.id
? 'text-green-400 cursor-not-allowed'
: 'text-green-600 hover:text-green-900'
}`}
title={edoc.acuse_descargado ? 'Acuse de EDoc ya descargado' : processingAcuseEdoc === edoc.id ? 'Procesando Acuse de EDoc...' : 'Procesar Acuse de EDoc'}
>
{processingAcuseEdoc === edoc.id ? (
<svg className="w-4 h-4 animate-spin" 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>
) : (
<svg className="w-4 h-4" 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>
)}
</button>
{/* Botón para cargar documentos al cove */}
<button
onClick={() => handleUploadToEdoc(edoc)}
className="p-1 text-green-600 rounded hover:text-green-900"
title="Cargar documentos a este cove"
>
<FileUp size={16} strokeWidth={2} />
</button>
{/* Botón Documentos VU Edocs */}
<button
onClick={() => handleShowEDocuments(edoc)}
className={`p-1 rounded transition-colors ${
!edoc.documentos || edoc.documentos.length === 0
? 'text-gray-400 cursor-not-allowed'
: 'text-blue-600 rounded hover:text-blue-900'
}`}
disabled={!edoc.documentos || edoc.documentos.length === 0}
title={!edoc.documentos || edoc.documentos.length === 0
? 'No hay documentos disponibles'
: edoc.documentos && edoc.documentos.length > 0
? `Ver ${edoc.documentos.length} documento(s) VU`
: 'Documentos VU Edocument'
}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11v5m0 0 2-2m-2 2-2-2M3 6v1a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1Zm2 2v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8H5Z"/>
</svg>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Paginación para EDocs */}
{edocsCount > 0 && (
<div className="flex items-center justify-between mt-6">
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-700">
Mostrando <span className="font-medium">{((edocsPage - 1) * edocsPageSize) + 1}</span> a{' '}
<span className="font-medium">{Math.min(edocsPage * edocsPageSize, edocsCount)}</span> de{' '}
<span className="font-medium">{edocsCount}</span> EDocs
</span>
<select
value={edocsPageSize}
onChange={(e) => {
setEdocsPageSize(Number(e.target.value));
setEdocsPage(1);
}}
className="ml-2 text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
>
<option value={10}>10 por página</option>
<option value={25}>25 por página</option>
<option value={50}>50 por página</option>
<option value={100}>100 por página</option>
</select>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setEdocsPage(Math.max(1, edocsPage - 1))}
disabled={edocsPage === 1}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<span className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300">
Página {edocsPage} de {Math.ceil(edocsCount / edocsPageSize)}
</span>
<button
onClick={() => setEdocsPage(Math.min(Math.ceil(edocsCount / edocsPageSize), edocsPage + 1))}
disabled={edocsPage >= Math.ceil(edocsCount / edocsPageSize)}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
{activeTab === 'auditor' && (
<div className="p-6">
{/* Header de la sección */}
<div className="mb-4 sm:mb-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<h3 className="text-lg font-semibold text-gray-900 sm:text-xl">
Auditoría del Pedimento
</h3>
</div>
</div>
</div>
{/* Contenido del Auditor */}
<div className="space-y-6">
{/* Resumen de Estado */}
<div className="p-6 border border-gray-200 shadow-lg bg-gradient-to-b from-white to-gray-50 rounded-xl">
<h4 className="flex items-center pb-4 mb-6 text-lg font-semibold text-gray-900 border-b">
<svg className="w-5 h-5 mr-2 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>
Resumen de Estado del Pedimento
</h4>
{dashboardSummary ? (
<>
{/* Indicador de Cumplimiento Principal */}
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-600">Cumplimiento Total</span>
<span className="text-2xl font-bold text-blue-600">{dashboardSummary.cumplimiento_total.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-500"
style={{ width: `${dashboardSummary.cumplimiento_total}%` }}
/>
</div>
</div>
{/* Grid de Estadísticas */}
<div className="grid grid-cols-2 gap-6 lg:grid-cols-3">
{/* Pedimentos */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Pedimentos</span>
<div className="flex items-center justify-center w-8 h-8 bg-green-100 rounded-full">
<svg className="w-4 h-4 text-green-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>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Completos</span>
<span className="font-medium">{dashboardSummary.pedimentos.completos}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pendientes</span>
<span className="font-medium text-yellow-600">{dashboardSummary.pedimentos.pendientes}</span>
</div>
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Cumplimiento</span>
<span className="text-sm font-semibold text-green-600">{dashboardSummary.pedimentos.cumplimiento.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-1.5 mt-1">
<div className="bg-green-500 h-1.5 rounded-full" style={{ width: `${dashboardSummary.pedimentos.cumplimiento}%` }} />
</div>
</div>
</div>
</div>
{/* COVEs */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">COVEs</span>
<div className="flex items-center justify-center w-8 h-8 bg-purple-100 rounded-full">
<svg className="w-4 h-4 text-purple-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>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Procesados</span>
<span className="font-medium">{dashboardSummary?.coves?.coves_procesados || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pendientes</span>
<span className="font-medium text-yellow-600">{dashboardSummary?.coves?.coves_pendientes || 0}</span>
</div>
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Cumplimiento</span>
<span className="text-sm font-semibold text-purple-600">{(dashboardSummary?.coves?.coves_cumplimiento || 0).toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-1.5 mt-1">
<div className="bg-purple-500 h-1.5 rounded-full" style={{ width: `${dashboardSummary?.coves?.coves_cumplimiento || 0}%` }} />
</div>
</div>
</div>
</div>
{/* Acuses de COVEs */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Acuses de COVEs</span>
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 rounded-full">
<svg className="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Procesados</span>
<span className="font-medium">{dashboardSummary?.acuse_coves?.acuse_coves_procesados || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pendientes</span>
<span className="font-medium text-yellow-600">{dashboardSummary?.acuse_coves?.acuse_coves_pendientes || 0}</span>
</div>
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Cumplimiento</span>
<span className="text-sm font-semibold text-indigo-600">{(dashboardSummary?.acuse_coves?.acuse_coves_cumplimiento || 0).toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-1.5 mt-1">
<div className="bg-indigo-500 h-1.5 rounded-full" style={{ width: `${dashboardSummary?.acuse_coves?.acuse_coves_cumplimiento || 0}%` }} />
</div>
</div>
</div>
</div>
{/* E-Documents */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">E-Documents</span>
<div className="flex items-center justify-center w-8 h-8 bg-blue-100 rounded-full">
<svg className="w-4 h-4 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>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Descargados</span>
<span className="font-medium">{dashboardSummary.edocuments.edocs_descargados}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pendientes</span>
<span className="font-medium text-yellow-600">{dashboardSummary.edocuments.edocs_pendientes}</span>
</div>
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Cumplimiento</span>
<span className="text-sm font-semibold text-blue-600">{dashboardSummary.edocuments.edocs_cumplimiento.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-1.5 mt-1">
<div className="bg-blue-500 h-1.5 rounded-full" style={{ width: `${dashboardSummary.edocuments.edocs_cumplimiento}%` }} />
</div>
</div>
</div>
</div>
{/* Acuses */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Acuses</span>
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 rounded-full">
<svg className="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Descargados</span>
<span className="font-medium">{dashboardSummary?.acuses?.acuse_descargados || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pendientes</span>
<span className="font-medium text-yellow-600">{dashboardSummary?.acuses?.acuses_pendientes || 0}</span>
</div>
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Cumplimiento</span>
<span className="text-sm font-semibold text-indigo-600">{(dashboardSummary?.acuses?.acuses_cumplimiento || 0).toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-1.5 mt-1">
<div className="bg-indigo-500 h-1.5 rounded-full" style={{ width: `${dashboardSummary?.acuses?.acuses_cumplimiento || 0}%` }} />
</div>
</div>
</div>
</div>
{/* Partidas */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Partidas</span>
<div className="flex items-center justify-center w-8 h-8 bg-orange-100 rounded-full">
<svg className="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Descargadas</span>
<span className="font-medium">{dashboardSummary.partidas.partidas_descargadas}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Pendientes</span>
<span className="font-medium text-yellow-600">{dashboardSummary.partidas.partidas_pendientes}</span>
</div>
<div className="pt-2 border-t">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Cumplimiento</span>
<span className="text-sm font-semibold text-orange-600">{dashboardSummary.partidas.cumplimiento.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-1.5 mt-1">
<div className="bg-orange-500 h-1.5 rounded-full" style={{ width: `${dashboardSummary.partidas.cumplimiento}%` }} />
</div>
</div>
</div>
</div>
{/* Estadísticas Generales */}
<div className="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Estadísticas Generales</span>
<div className="flex items-center justify-center w-8 h-8 bg-gray-100 rounded-full">
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Total Documentos</span>
<span className="font-medium">{dashboardSummary.documentos.descargados}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Total Remesas</span>
<span className="font-medium">{dashboardSummary.remesas.total}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Total Partidas</span>
<span className="font-medium">{dashboardSummary.partidas.total}</span>
</div>
</div>
</div>
</div>
</>
) : (
<div className="py-4 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" 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>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay datos disponibles</h3>
<p className="mt-1 text-sm text-gray-500">Intente recargar la página o contacte a soporte si el problema persiste.</p>
</div>
)}
</div>
{/* Análisis de Integridad */}
<div className="p-6 bg-white border shadow-lg rounded-xl">
<h4 className="flex items-center mb-4 text-lg font-semibold text-gray-900">
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Análisis de Integridad
</h4>
<div className="space-y-6">
{dashboardSummary && (
<>
{/* Estado General del Pedimento */}
<div className="flex items-center justify-between p-4 border border-gray-200 rounded-lg bg-gray-50">
<div className="flex items-center space-x-4">
<div className={`p-2 rounded-full ${
dashboardSummary.cumplimiento_total >= 100 ? 'bg-green-100' :
dashboardSummary.cumplimiento_total >= 75 ? 'bg-yellow-100' :
'bg-red-100'
}`}>
<svg className={`w-6 h-6 ${
dashboardSummary.cumplimiento_total >= 100 ? 'text-green-600' :
dashboardSummary.cumplimiento_total >= 75 ? 'text-yellow-600' :
'text-red-600'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d={dashboardSummary.cumplimiento_total >= 100
? "M5 13l4 4L19 7" // check mark
: dashboardSummary.cumplimiento_total >= 75
? "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" // warning
: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" // error
}
/>
</svg>
</div>
<div>
<h5 className="font-medium text-gray-900">Estado del Pedimento</h5>
<p className="text-sm text-gray-500">
{dashboardSummary.cumplimiento_total >= 100
? 'Todos los documentos y validaciones están completos'
: dashboardSummary.cumplimiento_total >= 75
? 'El pedimento requiere atención en algunos aspectos'
: 'Se requiere atención urgente en varios aspectos'}
</p>
</div>
</div>
<div className="text-right">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
dashboardSummary.cumplimiento_total >= 100 ? 'bg-green-100 text-green-800' :
dashboardSummary.cumplimiento_total >= 75 ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{dashboardSummary.cumplimiento_total.toFixed(1)}% Completado
</span>
</div>
</div>
{/* Análisis Detallado */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{/* Documentos Base */}
<div className={`p-4 rounded-lg border ${
dashboardSummary.pedimentos.cumplimiento >= 100 ? 'bg-green-50 border-green-200' : 'bg-yellow-50 border-yellow-200'
}`}>
<div className="flex items-start">
<svg className={`w-5 h-5 mt-0.5 mr-3 ${
dashboardSummary.pedimentos.cumplimiento >= 100 ? 'text-green-600' : 'text-yellow-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>
<h6 className={`font-medium ${
dashboardSummary.pedimentos.cumplimiento >= 100 ? 'text-green-800' : 'text-yellow-800'
}`}>Documentos Base</h6>
<p className="mt-1 text-sm text-gray-600">
{`${dashboardSummary.pedimentos.completos} de ${dashboardSummary.pedimentos.total} documentos procesados`}
</p>
</div>
</div>
</div>
{/* Validación de COVEs */}
<div className={`p-4 rounded-lg border ${
(dashboardSummary.coves.coves_cumplimiento >= 100 &&
dashboardSummary.acuse_coves.acuse_coves_cumplimiento >= 100) ?
'bg-green-50 border-green-200' : 'bg-yellow-50 border-yellow-200'
}`}>
<div className="flex items-start">
<svg className={`w-5 h-5 mt-0.5 mr-3 ${
(dashboardSummary.coves.coves_cumplimiento >= 100 &&
dashboardSummary.acuse_coves.acuse_coves_cumplimiento >= 100) ?
'text-green-600' : 'text-yellow-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>
<h6 className={`font-medium ${
(dashboardSummary.coves.coves_cumplimiento >= 100 &&
dashboardSummary.acuse_coves.acuse_coves_cumplimiento >= 100) ?
'text-green-800' : 'text-yellow-800'
}`}>Validación de COVEs</h6>
<div className="mt-1 space-y-1">
<p className="text-sm text-gray-600">
{`COVEs: ${dashboardSummary.coves.coves_procesados} de ${dashboardSummary.coves.total} procesados`}
</p>
<p className="text-sm text-gray-600">
{`Acuses: ${dashboardSummary.acuse_coves.acuse_coves_procesados} de ${dashboardSummary.acuse_coves.total} procesados`}
</p>
</div>
</div>
</div>
</div>
{/* E-Documents y Acuses */}
<div className={`p-4 rounded-lg border ${
(dashboardSummary.edocuments.edocs_cumplimiento >= 100 &&
dashboardSummary.acuses.acuses_cumplimiento >= 100) ?
'bg-green-50 border-green-200' : 'bg-yellow-50 border-yellow-200'
}`}>
<div className="flex items-start">
<svg className={`w-5 h-5 mt-0.5 mr-3 ${
(dashboardSummary.edocuments.edocs_cumplimiento >= 100 &&
dashboardSummary.acuses.acuses_cumplimiento >= 100) ?
'text-green-600' : 'text-yellow-600'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
</svg>
<div>
<h6 className={`font-medium ${
(dashboardSummary.edocuments.edocs_cumplimiento >= 100 &&
dashboardSummary.acuses.acuses_cumplimiento >= 100) ?
'text-green-800' : 'text-yellow-800'
}`}>E-Documents y Acuses</h6>
<div className="mt-1 space-y-1">
<p className="text-sm text-gray-600">
{`E-Docs: ${dashboardSummary.edocuments.edocs_descargados} de ${dashboardSummary.edocuments.total} descargados`}
</p>
<p className="text-sm text-gray-600">
{`Acuses: ${dashboardSummary.acuses.acuse_descargados} de ${dashboardSummary.acuses.total} descargados`}
</p>
</div>
</div>
</div>
</div>
{/* Partidas */}
<div className={`p-4 rounded-lg border ${
dashboardSummary.partidas.cumplimiento >= 100 ? 'bg-green-50 border-green-200' : 'bg-yellow-50 border-yellow-200'
}`}>
<div className="flex items-start">
<svg className={`w-5 h-5 mt-0.5 mr-3 ${
dashboardSummary.partidas.cumplimiento >= 100 ? 'text-green-600' : 'text-yellow-600'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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>
<h6 className={`font-medium ${
dashboardSummary.partidas.cumplimiento >= 100 ? 'text-green-800' : 'text-yellow-800'
}`}>Validación de Partidas</h6>
<p className="mt-1 text-sm text-gray-600">
{`${dashboardSummary.partidas.partidas_descargadas} de ${dashboardSummary.partidas.total} partidas procesadas`}
</p>
</div>
</div>
</div>
</div>
{/* Recomendaciones */}
{(dashboardSummary.cumplimiento_total < 100) && (
<div className="p-4 mt-4 border border-blue-200 rounded-lg bg-blue-50">
<h6 className="flex items-center mb-2 font-medium text-blue-800">
<svg className="w-5 h-5 mr-2 text-blue-600" 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>
Recomendaciones
</h6>
<ul className="space-y-2 text-sm text-blue-900">
{dashboardSummary.coves.coves_cumplimiento < 100 && (
<li> Procesar los COVEs pendientes ({dashboardSummary.coves.coves_pendientes} restantes)</li>
)}
{dashboardSummary.acuse_coves.acuse_coves_cumplimiento < 100 && (
<li> Obtener los acuses de COVEs faltantes ({dashboardSummary.acuse_coves.acuse_coves_pendientes} restantes)</li>
)}
{dashboardSummary.edocuments.edocs_cumplimiento < 100 && (
<li> Completar la descarga de E-Documents ({dashboardSummary.edocuments.edocs_pendientes} pendientes)</li>
)}
{dashboardSummary.acuses.acuses_cumplimiento < 100 && (
<li> Obtener los acuses de E-Documents ({dashboardSummary.acuses.acuses_pendientes} pendientes)</li>
)}
{dashboardSummary.partidas.cumplimiento < 100 && (
<li> Procesar las partidas pendientes ({dashboardSummary.partidas.partidas_pendientes} restantes)</li>
)}
</ul>
</div>
)}
</>
)}
</div>
</div>
{/* Timeline de Actividades */}
<div className="p-6 bg-white border shadow-lg rounded-xl">
<h4 className="flex items-center mb-4 text-lg font-semibold text-gray-900">
<svg className="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Timeline de Actividades Recientes
</h4>
<div className="flex flex-col items-center justify-center py-6">
<div className="flex items-center justify-center w-16 h-16 mb-4 bg-blue-100 rounded-full">
<svg className="w-8 h-8 text-blue-600 animate-spin" 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>
</div>
<p className="text-lg font-medium text-gray-900">Trabajando en ello</p>
<p className="mt-2 text-sm text-gray-500">Estamos preparando el historial de actividades</p>
<p className="mt-4 text-sm text-blue-600">Esta funcionalidad estará disponible próximamente</p>
</div>
</div>
</div>
</div>
)}
{/* NUEVO CONTENIDO PARA TAB DE ERRORES VU */}
{activeTab === 'errores' && (
<div className="p-6">
{/* Header de la sección */}
<div className="mb-4 space-y-3 sm:mb-6 sm:space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<h3 className="text-lg font-semibold text-gray-900 sm:text-xl">
Documentos de Error VU
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 w-fit">
{errorDocsCount} documentos
</span>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:gap-3">
{errorDocuments.length > 0 && (
<>
<button
onClick={downloadAllErrors}
disabled={downloadingAllErrors}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50"
>
{downloadingAllErrors ? (
<>
<svg className="w-4 h-4 mr-2 -ml-1 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" 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>
<span className="hidden sm:inline">Descargando...</span>
<span className="sm:hidden">Descargando...</span>
</>
) : (
<>
<svg className="w-4 h-4 mr-1 sm: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>
<span className="hidden sm:inline">Descargar Todos</span>
<span className="sm:hidden">Descargar</span>
</>
)}
</button>
<button
onClick={() => setShowErrorFilters(!showErrorFilters)}
className="inline-flex items-center justify-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<svg className="w-4 h-4 mr-1 sm:mr-2" 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>
<span className="hidden sm:inline">{showErrorFilters ? 'Ocultar Filtros' : 'Mostrar Filtros'}</span>
<span className="sm:hidden">Filtros</span>
</button>
</>
)}
</div>
</div>
{/* Filtros expandibles para errores */}
{showErrorFilters && (
<div className="grid grid-cols-1 gap-4 p-4 border rounded-lg md:grid-cols-2 lg:grid-cols-4 bg-red-50">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Buscar por nombre
</label>
<input
type="text"
value={errorFilters.name}
onChange={(e) => setErrorFilters(prev => ({ ...prev, name: e.target.value }))}
placeholder="Nombre del archivo..."
className="w-full px-3 py-2 placeholder-gray-400 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
/>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Extensión
</label>
<select
value={errorFilters.extension}
onChange={(e) => setErrorFilters(prev => ({ ...prev, extension: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
>
<option value="">Todas las extensiones</option>
<option value="pdf">PDF</option>
<option value="xml">XML</option>
<option value="jpg">JPG</option>
<option value="png">PNG</option>
<option value="doc">DOC</option>
<option value="docx">DOCX</option>
<option value="xls">XLS</option>
<option value="xlsx">XLSX</option>
<option value="txt">TXT</option>
<option value="csv">CSV</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fuente
</label>
<select
value={errorFilters.fuente || ''}
onChange={(e) => setErrorFilters(prev => ({ ...prev, fuente: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
>
<option value="">Todas las fuentes</option>
<option value="1">Manual</option>
<option value="2">VU</option>
<option value="3">Importación</option>
<option value="4">Sistema</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Tipo de Error
</label>
<select
value={errorFilters.tipo_error || ''}
onChange={(e) => setErrorFilters(prev => ({ ...prev, tipo_error: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
>
<option value="">Todos los errores</option>
<option value="14">Error Pedimento Completo VU</option>
<option value="16">Error Remesa VU</option>
<option value="18">Error Partidas VU</option>
<option value="20">Error COVES VU</option>
<option value="22">Error Edocuments VU</option>
<option value="24">Error Acuse COVES VU</option>
<option value="26">Error Acuse Edocuments VU</option>
</select>
</div>
{/* <div>
<label className="block mb-1 text-sm font-medium text-gray-700">
Fecha de creación
</label>
<input
type="date"
value={errorFilters.date}
onChange={(e) => setErrorFilters(prev => ({ ...prev, date: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm"
/>
</div> */}
<div className="flex items-end">
<button
onClick={() => setErrorFilters({
name: '',
document_type: '',
extension: '',
date: '',
created_at__gte: '',
created_at__lte: '',
fuente: '',
pedimento_numero: ''
})}
className="w-full px-3 py-2 text-sm font-medium text-gray-600 transition-colors bg-gray-100 rounded-md hover:bg-gray-200"
>
Limpiar filtros
</button>
</div>
</div>
)}
</div>
{/* Área de acciones para documentos de error seleccionados */}
{selectedErrorDocuments.length > 0 && (
<div className="mb-4 overflow-hidden border border-red-200 bg-gradient-to-r from-red-50 to-pink-50 rounded-2xl">
<div className="px-6 py-4 border-b border-red-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-full">
<svg className="w-5 h-5 text-red-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">
{selectedErrorDocuments.length} documento{selectedErrorDocuments.length !== 1 ? 's' : ''} de error seleccionado{selectedErrorDocuments.length !== 1 ? 's' : ''}
</h3>
<p className="text-sm text-gray-600">Selecciona una acción para continuar</p>
</div>
</div>
<button
onClick={() => {
setSelectedErrorDocuments([]);
setIsSelectAllErrorDocs(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={handleDeleteSelectedErrorDocuments}
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>
)}
{errorDocsLoading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-b-2 border-red-600 rounded-full animate-spin"></div>
<span className="ml-2 text-gray-600">Cargando documentos de error...</span>
</div>
) : errorDocsError ? (
<div className="py-12 text-center">
<div className="mb-2 text-red-600">
<svg className="w-12 h-12 mx-auto" 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.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="mb-1 text-lg font-medium text-gray-900">Error al cargar documentos de error</h3>
<p className="text-gray-600">{errorDocsError}</p>
</div>
) : errorDocuments.length === 0 ? (
<div className="py-12 text-center">
<svg className="w-12 h-12 mx-auto text-gray-400" 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>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay documentos de error</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron documentos de error VU para este pedimento.
</p>
</div>
) : (
<div className="space-y-4">
{/* Tabla de documentos de error */}
<div className="overflow-hidden rounded-lg shadow ring-1 ring-black ring-opacity-5">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<input
type="checkbox"
checked={isSelectAllErrorDocs}
onChange={handleSelectAllErrorDocuments}
className="w-4 h-4 text-red-600 border-gray-300 rounded focus:ring-red-500"
/>
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Documento
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Tipo
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Extensión
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Tamaño
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fuente
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
Fecha de Creación
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Acciones</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{errorDocuments.map((doc, index) => (
<tr key={`${doc.id}-${index}`} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedErrorDocuments.includes(doc.id)}
onChange={() => handleSelectErrorDocument(doc.id)}
className="w-4 h-4 text-red-600 border-gray-300 rounded focus:ring-red-500"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 w-10 h-10">
<div className="flex items-center justify-center w-10 h-10 bg-red-100 rounded-lg">
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900" title={doc.archivo}>
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
</div>
<div className="text-sm text-gray-500">
Pedimento: {doc.pedimento_numero || 'N/A'}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{getDocumentTypeName(doc.document_type)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{doc.extension ? doc.extension.toUpperCase() : 'N/A'}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-900 whitespace-nowrap">
{formatFileSize(doc.size)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{doc.fuente_nombre}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{formatDate(doc.created_at)}
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<div className="flex items-center space-x-2">
<button
onClick={() => previewDocument(doc)}
className="p-1 text-blue-600 rounded hover:text-blue-900"
title="Vista previa"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button
onClick={() => downloadDocument(doc)}
className="p-1 text-green-600 rounded hover:text-green-900"
title="Descargar"
>
<svg className="w-4 h-4" 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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para documentos de error */}
{errorDocsCount > 0 && (
<div className="flex flex-col gap-4 mt-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span className="text-sm text-gray-700">
<span className="hidden sm:inline">Mostrando </span>
<span className="font-medium">{((errorDocsPage - 1) * errorDocsPageSize) + 1}</span>-
<span className="font-medium">{Math.min(errorDocsPage * errorDocsPageSize, errorDocsCount)}</span>
<span className="hidden sm:inline">de </span>
<span className="sm:hidden">/</span>
<span className="font-medium">{errorDocsCount}</span>
<span className="hidden sm:inline"> documentos</span>
</span>
<select
value={errorDocsPageSize}
onChange={(e) => {
setErrorDocsPageSize(Number(e.target.value));
setErrorDocsPage(1);
}}
className="w-full text-sm border-gray-300 rounded-md focus:ring-red-500 focus:border-red-500 sm:w-auto"
>
<option value={10}>10 por página</option>
<option value={25}>25 por página</option>
<option value={50}>50 por página</option>
<option value={100}>100 por página</option>
</select>
</div>
<div className="flex items-center justify-center space-x-1 sm:justify-end sm:space-x-2">
<button
onClick={() => setErrorDocsPage(Math.max(1, errorDocsPage - 1))}
disabled={errorDocsPage === 1}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<span className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300">
Página {errorDocsPage} de {Math.ceil(errorDocsCount / errorDocsPageSize)}
</span>
<button
onClick={() => setErrorDocsPage(Math.min(Math.ceil(errorDocsCount / errorDocsPageSize), errorDocsPage + 1))}
disabled={errorDocsPage >= Math.ceil(errorDocsCount / errorDocsPageSize)}
className="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Modal de vista previa mejorado */}
{previewOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="bg-white/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-blue-200/50 overflow-hidden relative flex flex-col w-full max-w-6xl h-full max-h-[95vh]">
{/* Header del modal con gradiente e información del archivo */}
<div className="flex items-center justify-between px-6 py-4 text-white bg-gradient-to-r from-blue-600 to-blue-800">
<div className="flex items-center flex-1 min-w-0 gap-4">
<div className="flex-shrink-0 p-2 bg-white/20 rounded-xl">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-xl font-bold truncate">Vista previa de documento</h3>
{previewDoc && (
<div className="flex items-center gap-3 mt-1 text-sm text-blue-100">
<span className="truncate">{previewDoc.archivo_original || 'Sin nombre'}</span>
{previewDoc.size && (
<span className="bg-white/20 px-2 py-0.5 rounded-lg text-xs font-medium">
{formatFileSize(previewDoc.size)}
</span>
)}
{previewDoc.extension && (
<span className="bg-white/20 px-2 py-0.5 rounded-lg text-xs font-medium uppercase">
{previewDoc.extension}
</span>
)}
</div>
)}
</div>
</div>
{/* Controles del header */}
<div className="flex items-center flex-shrink-0 gap-2">
{/* Botón de descarga siempre visible */}
{previewDoc && (
<button
onClick={() => downloadDocument(previewDoc)}
className="p-2 transition-colors duration-200 bg-white/20 hover:bg-white/30 rounded-xl group"
title="Descargar documento"
>
<svg className="w-5 h-5 transition-transform group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</button>
)}
{/* Controles de zoom para imágenes */}
{previewType === 'img' && (
<div className="flex items-center gap-1 p-1 bg-white/20 rounded-xl">
<button
onClick={() => setImageZoom(Math.max(0.2, imageZoom - 0.2))}
className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-xs"
title="Alejar"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 12H4" />
</svg>
</button>
<span className="px-2 text-xs font-medium">{Math.round(imageZoom * 100)}%</span>
<button
onClick={() => setImageZoom(Math.min(3, imageZoom + 0.2))}
className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-xs"
title="Acercar"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</button>
<button
onClick={() => setImageZoom(1)}
className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-xs"
title="Tamaño original"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 8V4m0 0h4M4 4l5 5m11-5v4m0-4h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
</button>
</div>
)}
{/* Botón cerrar */}
<button
onClick={handleClosePreview}
className="p-2 transition-colors duration-200 bg-white/20 hover:bg-red-500/80 rounded-xl"
title="Cerrar vista previa"
>
<svg className="w-5 h-5" 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 */}
<div className="flex flex-col flex-1 min-h-0 bg-gray-50">
{previewLoading ? (
<div className="flex items-center justify-center flex-1">
<div className="text-center">
<div className="inline-flex items-center space-x-3 text-blue-600">
<svg className="w-10 h-10 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>
<span className="text-lg font-medium">Cargando documento...</span>
</div>
<p className="mt-2 text-gray-500">Preparando vista previa</p>
</div>
</div>
) : previewError ? (
<div className="flex items-center justify-center flex-1">
<div className="max-w-md text-center text-red-600">
<div className="flex items-center justify-center w-20 h-20 mx-auto mb-4 rounded-full bg-red-50">
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h4 className="mb-2 text-lg font-semibold">Error al cargar documento</h4>
<p className="text-sm text-gray-600">{previewError}</p>
<button
onClick={() => setPreviewError(null)}
className="px-4 py-2 mt-4 text-red-800 transition-colors bg-red-100 rounded-lg hover:bg-red-200"
>
Reintentar
</button>
</div>
</div>
) : previewType === 'pdf' ? (
<div className="flex-1 bg-white">
<iframe
src={previewUrl}
title="PDF Preview"
className="w-full h-full border-0"
style={{ minHeight: '500px' }}
/>
</div>
) : previewType === 'img' ? (
<div className="flex items-center justify-center flex-1 p-4 overflow-auto bg-gray-100">
<div className="relative">
<img
src={previewUrl}
alt="Vista previa"
className="border-2 border-white rounded-lg shadow-lg max-w-none"
style={{
transform: `scale(${imageZoom})`,
transformOrigin: 'center',
transition: 'transform 0.2s ease'
}}
/>
</div>
</div>
) : previewType === 'xml' ? (
// <div className="flex flex-col flex-1 bg-white">
<div className="flex flex-col h-full bg-white">
<div className="flex items-center justify-between px-6 py-3 bg-gray-100 border-b border-gray-200">
<div className="flex items-center gap-2">
<div className="bg-green-100 p-1.5 rounded-lg">
<svg className="w-4 h-4 text-green-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>
<span className="font-medium text-gray-700">Documento XML</span>
</div>
<button
className="inline-flex items-center gap-2 px-4 py-2 text-sm text-blue-600 transition-colors border border-blue-300 rounded-lg hover:text-blue-800 bg-blue-50 hover:bg-blue-100"
onClick={() => {
navigator.clipboard.writeText(previewXml);
// Mostrar notificación de copiado
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Copiar XML
</button>
</div>
<pre
className="flex-1 p-6 overflow-x-auto overflow-y-auto text-sm bg-white border-l-4 border-green-400"
// className="flex-1 p-6 overflow-auto text-sm bg-white border-l-4 border-green-400"
style={{
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
lineHeight: '1.6',
// minHeight: '400px',
width: '100%',
whiteSpace: 'pre'
}}
dangerouslySetInnerHTML={{ __html: previewXmlHtml }}
/>
</div>
) : previewType === 'txt' ? (
<div className="flex-1 p-6 overflow-auto bg-white">
<div className="p-4 border border-gray-200 rounded-lg bg-gray-50">
<div className="flex items-center gap-2 mb-4">
<div className="bg-blue-100 p-1.5 rounded-lg">
<svg className="w-4 h-4 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>
<span className="font-medium text-gray-700">Documento de texto</span>
</div>
<pre className="font-mono text-sm leading-relaxed text-gray-800 whitespace-pre-wrap">
{previewContent || 'Contenido no disponible'}
</pre>
</div>
</div>
) : previewUrl ? (
<div className="flex items-center justify-center flex-1 p-8 bg-gray-50">
<div className="text-center">
<div className="flex items-center justify-center w-20 h-20 mx-auto mb-6 rounded-full bg-blue-50">
<svg className="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h4 className="mb-2 text-lg font-semibold text-gray-900">Vista previa no disponible</h4>
<p className="mb-6 text-gray-600">Este tipo de archivo no se puede mostrar en el navegador</p>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-6 py-3 space-x-2 font-medium text-white transition-colors bg-blue-600 shadow-lg hover:bg-blue-700 rounded-xl hover:shadow-xl"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>Descargar archivo</span>
</a>
</div>
</div>
) : (
<div className="flex items-center justify-center flex-1 p-8">
<div className="text-center text-gray-500">
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300" 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>
<p className="text-lg">No se pudo cargar el documento</p>
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Modal para subir documentos */}
{showUploadModal && (
<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-lg 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-green-100 rounded-full">
<svg className="w-6 h-6 text-green-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 Documentos
</h3>
<p className="text-sm text-gray-600">Selecciona los archivos a subir</p>
</div>
</div>
</div>
{/* Contenido del modal */}
<div className="px-6 py-4">
<div className="mb-4">
<label className="block mb-2 text-sm font-medium text-gray-700">
Seleccionar archivos
</label>
<input
type="file"
multiple
onChange={handleFileSelect}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{selectedFiles.length > 0 && (
<div className="p-3 mt-3 rounded-lg bg-gray-50">
<p className="mb-2 text-sm font-medium text-gray-700">
Archivos seleccionados ({selectedFiles.length}):
</p>
<div className="overflow-y-auto max-h-32">
{selectedFiles.map((file, index) => (
<div key={index} className="py-1 text-xs text-gray-600 border-b border-gray-200 last:border-b-0">
{file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)
</div>
))}
</div>
</div>
)}
<div className="p-3 mt-4 border border-blue-200 rounded-lg bg-blue-50">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" 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>
<div>
<p className="text-sm font-medium text-blue-800">Información</p>
<p className="mt-1 text-sm text-blue-700">
{activeTab === 'pedimento'
? 'Para que los archivos de pedimento completo y/o remesa se vean en este apartado, deben tener la siguiente nomenclatura en el nombre Pedimento Completo ej: vu_PC_26-01-1234-1234567.xml y para Remesa ej: vu_RM_26-01-1234-1234567.xml, El 26-01-1234-1234567 es el pedimento al que se esta subiendo el documento.'
: 'Los archivos se subirán al pedimento actual. Se aceptan múltiples formatos de archivo.' }
</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={() => {
setShowUploadModal(false);
setSelectedFiles([]);
}}
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={handleUploadDocuments}
disabled={selectedFiles.length === 0 || uploadingDocuments}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white transition-colors duration-200 bg-green-600 border border-transparent rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{uploadingDocuments ? (
<>
<svg className="w-4 h-4 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>
Subiendo...
</>
) : (
<>
<svg className="w-4 h-4" 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>
Subir {selectedFiles.length} archivo{selectedFiles.length !== 1 ? 's' : ''}
</>
)}
</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={confirmDeleteDocuments}
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>
)}
{/* Modal para mostrar documentos */}
{showDocumentsModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in">
<div className="w-full max-w-md p-6 scale-95 bg-white rounded-lg shadow-xl animate-modal-slide-up">
<div className="flex items-center justify-between mb-4">
<h3 className="mb-2 text-lg font-semibold text-blue-900">
Documentos VU - {getTabName(activeTab)}: {selectedVUNumber}
</h3>
</div>
<div className="mb-4">
<p className="mb-2 text-sm text-gray-600">
Total: {selectedVUDocuments.length} documento(s)
</p>
<div className="overflow-y-auto max-h-64">
<ul className="space-y-2">
{selectedVUDocuments.map((doc, index) => {
const nombreCompleto = doc.archivo || doc.nombre || 'Documento sin nombre';
const nombreArchivo = nombreCompleto.split('/').pop();
const extension = nombreArchivo.split('.').pop().toUpperCase();
return (
<li key={index} className="flex items-center p-2 rounded bg-gray-50">
<div className="flex items-center justify-center flex-shrink-0 w-8 h-8 mr-3 bg-blue-100 rounded">
<span className="text-xs font-medium text-blue-600">{extension}</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate" title={nombreArchivo}>
{nombreArchivo}
</p>
{doc.tipo && (
<p className="text-xs text-gray-500">Tipo: {doc.tipo}</p>
)}
</div>
<div className="flex items-center ml-2 space-x-1">
{/* Botón de vista previa */}
<button
onClick={() => previewDocumentVU(doc)}
className="p-1.5 text-blue-600 rounded-lg hover:text-blue-900 hover:bg-blue-100 transition-colors"
title="Vista previa"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
{/* Botón de descarga */}
<button
onClick={() => downloadDocumentVU(doc)}
className="p-1.5 text-green-600 rounded-lg hover:text-green-900 hover:bg-green-100 transition-colors"
title="Descargar"
>
<svg className="w-4 h-4" 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>
</div>
</li>
);
})}
</ul>
</div>
</div>
<div className="flex justify-end space-x-2">
{/* Botón Descargar Todos - al lado izquierdo del botón Cerrar */}
<button
onClick={downloadAllVU}
disabled={selectedVUDocuments.length === 0}
className={`px-3 py-1.5 text-sm font-medium text-white rounded-md transition-colors flex items-center justify-center ${
selectedVUDocuments.length === 0
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700 shadow-sm'
}`}
title={selectedVUDocuments.length === 0 ? 'No hay documentos para descargar' : 'Descargar todos los documentos'}
>
<svg className="w-4 h-4 mr-1" 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>
<span>Descargar Todos</span>
</button>
{/* Botón Cerrar - al lado derecho */}
<button
onClick={() => {
setShowDocumentsModal(false);
setSelectedVUDocuments([]);
setSelectedVUNumber('');
}}
className="px-3 py-1.5 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 transition-colors shadow-sm flex items-center justify-center"
title="Cerrar ventana"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span>Cerrar</span>
</button>
</div>
</div>
</div>
)}
{/* Modal para subir documentos */}
{uploadModalOpen && selectedDocumentForUpload && (
<div className="fixed inset-0 z-50 overflow-y-auto bg-black bg-opacity-50">
<div className="flex items-center justify-center min-h-screen p-4">
<div className="relative w-full max-w-md p-6 bg-white rounded-lg shadow-xl">
<div className="mb-4">
<h3 className="text-lg font-medium leading-6 text-gray-900">
Subir documentos a {selectedDocumentForUpload.tab}: {selectedDocumentForUpload.numero}
</h3>
<p className="mt-1 text-sm text-gray-500">
Selecciona los archivos que deseas subir
</p>
</div>
{/* Selectores específicos por tipo de archivo */}
<div className="space-y-4">
{/* Primer selector para COVE (XML solo para partida y cove) */}
{(isPartida || isCove) && (
<div>
<label className="block mb-2 text-sm font-medium text-gray-700">
Seleccionar archivo (XML)
</label>
<div className="relative">
<input
type="file"
accept=".xml"
onChange={(e) => {handleUploadFileSelect(e,'.xml','general')}}
className="block w-full px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<div className="mt-1 text-xs text-gray-500">
Solo archivo XML (.xml)
</div>
</div>
</div>
)}
{/* Primer selector (PDF solo edoc) */}
{(isEdoc) && (
<div>
<label className="block mb-2 text-sm font-medium text-gray-700">
Seleccionar archivo Edocument (PDF)
</label>
<div className="relative">
<input
type="file"
accept=".pdf"
onChange={(e) => {handleUploadFileSelect(e,'.pdf','general')}}
className="block w-full px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<div className="mt-1 text-xs text-gray-500">
Solo archivo PDF (.pdf)
</div>
</div>
</div>
)}
{/* Segundo selector para Acuse (PDF solo para cove y edoc) */}
{(isCove || isEdoc) && (
<div>
<label className="block mb-2 text-sm font-medium text-gray-700">
Seleccionar archivo Acuse (PDF)
</label>
<div className="relative">
<input
type="file"
accept=".pdf"
onChange={(e) => {handleUploadFileSelect(e,'.pdf','acuse')}}
className="block w-full px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
/>
<div className="mt-1 text-xs text-gray-500">
Solo archivo PDF (.pdf)
</div>
</div>
</div>
)}
{/* Lista de archivos seleccionados */}
{/* {uploadFiles.length > 0 && (
<div className="mt-4">
<h4 className="mb-2 text-sm font-medium text-gray-700">
Archivos seleccionados ({uploadFiles.length})
</h4>
<div className="overflow-y-auto border border-gray-200 rounded-md max-h-40">
<ul className="py-1 text-sm text-gray-700">
{uploadFiles.map((file, index) => (
<li key={index} className="flex items-center justify-between px-3 py-2 border-b border-gray-100 last:border-b-0">
<span className="truncate">{file.name}</span>
<span className="ml-2 text-xs text-gray-500">
{file.name.toLowerCase().endsWith('.xml') ? 'COVE' :
file.name.toLowerCase().endsWith('.pdf') ? 'Acuse' : 'Otro'}
</span>
</li>
))}
</ul>
</div>
</div>
)} */}
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => {
setUploadModalOpen(false);
setShowUploadModal(false);
setSelectedFiles([]);
}}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Cancelar
</button>
<button
onClick={() => {handleUploadFilesToDocumento(selectedDocumentForUpload.tab)}}
disabled={uploadFiles.length === 0 || uploadingToDocumento === selectedDocumentForUpload.id}
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{uploadingToDocumento === selectedDocumentForUpload.id ? (
<>
<svg className="w-4 h-4 mr-2 -ml-1 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" 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>
Subiendo...
</>
) : (
`Subir ${uploadFiles.length} archivo${uploadFiles.length !== 1 ? 's' : ''}`
)}
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}