Files
frontend/src/pages/PedimentoDetail.jsx
JCedillo 5f4a797c3c refactor: centralize download functions into utils
- Create src/utils/downloadUtils.js with downloadFile and downloadBulkZip
- Remove duplicated download functions from PedimentoDetail.jsx
- Remove duplicated download functions from Documents.jsx
- Add proper imports to use centralized functions
- Improve code reusability and maintainability
- Ensure consistent download behavior across components
2025-10-13 14:25:01 -05:00

4801 lines
250 KiB
JavaScript

import React, { useEffect, useState, useRef } from '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 { fetchWithAuth, postWithAuth, putWithAuth } 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 [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 [dashboardSummary, setDashboardSummary] = useState(null);
const [showFilters, setShowFilters] = 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: ''
});
// 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 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);
// 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 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);
}
};
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'
};
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');
}
};
// 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;
// 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 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
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);
};
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);
}
};
// Estados de carga
if (loading) return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600 text-lg">Cargando detalle del pedimento...</p>
</div>
</div>
);
if (error) return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6 flex items-center justify-center">
<div className="text-center">
<div className="bg-red-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<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="text-xl font-semibold text-gray-900 mb-2">Error al cargar</h2>
<p className="text-gray-600">{error}</p>
<Link to="/expedientes" className="mt-4 inline-flex items-center text-blue-600 hover:text-blue-800">
Volver a expedientes
</Link>
</div>
</div>
);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 mb-6 animate-fadein-slideup opacity-0"
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 text-blue-600 hover:text-blue-800 transition-colors duration-200 mb-2"
>
<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="text-2xl lg:text-3xl font-bold text-gray-900 mb-1">
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 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
📄 {docsCount} documentos
</span>
</div>
)}
</div>
</div>
{/* Información del Pedimento */}
{pedimento && (
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 mb-6 animate-fadein-slideup opacity-0"
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 sm:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="bg-gradient-to-br from-blue-50 to-blue-100/50 p-4 rounded-xl border border-blue-200/50 shadow-sm">
<dt className="text-sm font-semibold text-blue-700 mb-2 flex items-center">
<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="bg-gradient-to-br from-green-50 to-green-100/50 p-4 rounded-xl border border-green-200/50 shadow-sm">
<dt className="text-sm font-semibold text-green-700 mb-2 flex items-center">
<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="bg-gradient-to-br from-purple-50 to-purple-100/50 p-4 rounded-xl border border-purple-200/50 shadow-sm">
<dt className="text-sm font-semibold text-purple-700 mb-2 flex items-center">
<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="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 animate-fadein-slideup opacity-0"
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 space-x-2 sm:space-x-4 lg:space-x-8 px-3 sm:px-6 overflow-x-auto scrollbar-hide" aria-label="Pestañas">
<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>
<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>
<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>
<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>
<button
onClick={() => handleTabChange('procesos')}
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 === 'procesos'
? '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-6 9l2 2 4-4" />
</svg>
<span className="hidden sm:inline">Procesos</span>
<span className="sm:hidden">Proc.</span>
</div>
</button>
<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 */}
<div className="mb-4 sm:mb-6 space-y-3 sm:space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
Documentos 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">
{docsCount} documentos
</span>
</div>
{documents.length > 0 && (
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<button
onClick={downloadAll}
disabled={downloadingAll}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{downloadingAll ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white 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 md:grid-cols-2 lg:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg border">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
>
Limpiar filtros
</button>
</div>
</div>
)}
</div>
{/* Área de acciones para documentos seleccionados */}
{selectedDocuments.length > 0 && (
<div className="mb-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-2xl overflow-hidden">
<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="bg-blue-100 rounded-full p-2">
<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 hover:text-gray-600 transition-colors"
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 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors duration-200 shadow-sm 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 justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Cargando documentos...</span>
</div>
) : error ? (
<div className="text-center py-12">
<div className="text-red-600 mb-2">
<svg className="mx-auto h-12 w-12" 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="text-lg font-medium text-gray-900 mb-1">Error al cargar documentos</h3>
<p className="text-gray-600">{error}</p>
</div>
) : documents.length === 0 ? (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 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 shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<input
type="checkbox"
checked={isSelectAllDocs}
onChange={handleSelectAllDocuments}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button
onClick={() => handleSort('archivo')}
className="group inline-flex items-center space-x-1 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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button
onClick={() => handleSort('document_type')}
className="group inline-flex items-center space-x-1 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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button
onClick={() => handleSort('extension')}
className="group inline-flex items-center space-x-1 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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button
onClick={() => handleSort('size')}
className="group inline-flex items-center space-x-1 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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fuente
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button
onClick={() => handleSort('created_at')}
className="group inline-flex items-center space-x-1 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="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<svg className="h-6 w-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 whitespace-nowrap text-sm text-gray-900">
{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 whitespace-nowrap text-sm text-gray-500">
{formatDate(doc.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => previewDocument(doc)}
className="text-blue-600 hover:text-blue-900 p-1 rounded"
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="text-green-600 hover:text-green-900 p-1 rounded"
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="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 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="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full 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 sm:justify-end space-x-1 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 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="h-5 w-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 border border-gray-300 bg-white text-sm font-medium text-gray-700">
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 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="h-5 w-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">
{/* Header de la sección */}
<div className="mb-4 sm:mb-6 space-y-3 sm:space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
Partidas 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 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{downloadingPartidas ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" 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 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-7 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
>
Limpiar filtros
</button>
</div>
</div>
</div>
{partidasLoading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Cargando partidas...</span>
</div>
) : partidasError ? (
<div className="text-center py-12">
<div className="text-red-600 mb-2">
<svg className="mx-auto h-12 w-12" 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="text-lg font-medium text-gray-900 mb-1">Error al cargar partidas</h3>
<p className="text-gray-600">{partidasError}</p>
</div>
) : !partidas || partidas.length === 0 ? (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 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">
{/* Tabla de partidas */}
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<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 left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
checked={allPartidasSelected}
onChange={handleSelectAllPartidas}
/>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Número de Partida
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado de Descarga
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha de Creación
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Última Actualización
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
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 left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 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 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-purple-100 flex items-center justify-center">
<svg className="h-6 w-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 whitespace-nowrap text-sm text-gray-500">
{partida.created_at ? formatDate(partida.created_at) : 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{partida.updated_at ? formatDate(partida.updated_at) : 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para partidas */}
{partidasCount > 0 && (
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 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="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full 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 sm:justify-end space-x-1 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 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="h-5 w-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 border border-gray-300 bg-white text-sm font-medium text-gray-700">
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 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="h-5 w-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">
{/* Header de la sección */}
<div className="mb-4 sm:mb-6 space-y-3 sm:space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
COVEs 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>
{/* Filtros */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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>
{covesLoading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Cargando COVEs...</span>
</div>
) : covesError ? (
<div className="text-center py-12">
<div className="text-red-600 mb-2">
<svg className="mx-auto h-12 w-12" 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="text-lg font-medium text-gray-900 mb-1">Error al cargar COVEs</h3>
<p className="text-gray-600">{covesError}</p>
</div>
) : coves.length === 0 ? (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 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 shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Número COVE
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha de Creación
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado COVE
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
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">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<svg className="h-6 w-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 whitespace-nowrap text-sm text-gray-500">
{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 whitespace-nowrap text-right text-sm font-medium">
<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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para COVEs */}
{covesCount > 0 && (
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 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="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full 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 sm:justify-end space-x-1 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 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="h-5 w-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 border border-gray-300 bg-white text-sm font-medium text-gray-700">
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 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="h-5 w-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">
{/* Header de la sección */}
<div className="mb-6 space-y-4">
<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 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 md:grid-cols-2 lg:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg border">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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 text-sm font-medium text-gray-700 mb-1">
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>
{edocsLoading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
<span className="ml-2 text-gray-600">Cargando EDocs...</span>
</div>
) : edocsError ? (
<div className="text-center py-12">
<div className="text-red-600 mb-2">
<svg className="mx-auto h-12 w-12" 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="text-lg font-medium text-gray-900 mb-1">Error al cargar EDocs</h3>
<p className="text-gray-600">{edocsError}</p>
</div>
) : edocs.length === 0 ? (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 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 shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Número EDocs
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Clave
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Descripción
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha de Creación
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado EDocs
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
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">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-green-100 flex items-center justify-center">
<svg className="h-6 w-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="px-6 py-4 whitespace-nowrap text-sm text-gray-900 max-w-xs truncate" title={edoc.descripcion}>
{edoc.descripcion || 'Sin descripción'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{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 whitespace-nowrap text-right text-sm font-medium">
<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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Paginación para EDocs */}
{edocsCount > 0 && (
<div className="mt-6 flex items-center justify-between">
<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 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="h-5 w-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 border border-gray-300 bg-white text-sm font-medium text-gray-700">
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 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="h-5 w-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 === 'procesos' && (
<div className="p-6">
{/* Header de la sección */}
<div className="mb-4 sm:mb-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
Procesos 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">
{procesosCount} Procesos
</span>
</div>
{/* Botones de creación de servicios */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-3">
{/* Botón prioritario: Obtener pedimento completo */}
{!existeServicio(3) && (
<button
onClick={() => handleCrearServicio(3, 'Procesamiento SAT')}
disabled={creatingService === 3}
className={`inline-flex items-center px-3 sm:px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white transition-colors duration-200 w-full sm:w-auto justify-center ${
creatingService === 3
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
}`}
>
{creatingService === 3 ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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">Creando...</span>
<span className="sm:hidden">Creando...</span>
</>
) : (
<>
<svg className="-ml-1 mr-2 h-4 w-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>
<span className="hidden sm:inline">Obtener Pedimento Completo</span>
<span className="sm:hidden">Obtener Pedimento</span>
</>
)}
</button>
)}
{/* Otros servicios disponibles */}
{existeServicio(3) && (
<div className="flex items-center space-x-2">
{/* Generación COVEs */}
{!existeServicio(4) && (
<button
onClick={() => handleCrearServicio(4, 'Generación COVEs')}
disabled={creatingService === 4}
className={`inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors duration-200 ${
creatingService === 4 ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{creatingService === 4 ? (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" 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="-ml-1 mr-2 h-4 w-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>
)}
COVEs
</button>
)}
{/* Generación EDocs */}
{!existeServicio(5) && (
<button
onClick={() => handleCrearServicio(5, 'Generación EDocs')}
disabled={creatingService === 5}
className={`inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors duration-200 ${
creatingService === 5 ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{creatingService === 5 ? (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" 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="-ml-1 mr-2 h-4 w-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>
)}
EDocs
</button>
)}
</div>
)}
</div>
</div>
</div>
{procesosLoading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
<span className="ml-2 text-gray-600">Cargando procesos...</span>
</div>
) : procesosError ? (
<div className="text-center py-12">
<div className="text-red-600 mb-2">
<svg className="mx-auto h-12 w-12" 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="text-lg font-medium text-gray-900 mb-1">Error al cargar procesos</h3>
<p className="text-gray-600">{procesosError}</p>
</div>
) : procesos.length === 0 ? (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 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-6 9l2 2 4-4" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay procesos</h3>
<p className="mt-1 text-sm text-gray-500">
No se encontraron procesos para este pedimento.
</p>
</div>
) : (
<div className="space-y-4">
{/* Tabla de Procesos */}
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
<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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID Proceso
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Servicio
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Organización
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fecha Creación
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Última Actualización
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{procesos.map((proceso, index) => (
<tr key={`${proceso.task_id}-${index}`} className={`hover:bg-gray-50 ${selectedProcesos.includes(proceso.task_id) ? 'bg-blue-50' : ''}`}>
<td className="px-3 py-4">
<div className="flex items-center justify-center">
<input
type="checkbox"
checked={selectedProcesos.includes(proceso.task_id)}
onChange={(e) => handleSelectProceso(proceso.task_id, e.target.checked)}
disabled={proceso.status === 'running' || proceso.status === 'completed'}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded disabled:opacity-50"
/>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-purple-100 flex items-center justify-center">
<svg className="h-6 w-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-6 9l2 2 4-4" />
</svg>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
#{proceso.task_id}
</div>
<div className="text-sm text-gray-500">
{proceso.pedimento_app}
</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 ${getServicioColor(proceso.servicio)}`}>
{getServicioLabel(proceso.servicio)}
</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 ${getTaskStatusColor(proceso.status)}`}>
{getTaskStatusLabel(proceso.status)}
</span>
{proceso.status === 'running' && (
<span className="ml-2 inline-flex items-center">
<svg className="animate-spin h-4 w-4 text-blue-600" 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>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{proceso.organizacion_name || 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(proceso.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(proceso.updated_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
{/* Botón Play (Ejecutar Servicio) */}
{isTaskActionable(proceso.status) && (
<button
onClick={() => handleEjecutarProceso(proceso)}
disabled={executingId === proceso.task_id || proceso.status === 'running'}
className={`group inline-flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200 ${
executingId === proceso.task_id || proceso.status === 'running'
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'text-green-700 bg-green-100 hover:bg-green-200 hover:text-green-800'
}`}
title={
executingId === proceso.task_id ? 'Ejecutando...' :
proceso.status === 'running' ? 'En proceso...' :
'Ejecutar servicio'
}
>
{executingId === proceso.task_id ? (
<svg className="animate-spin h-4 w-4" 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="h-4 w-4 transition-transform group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</button>
)}
{/* Botón Retry (Reintentar) */}
{proceso.status === 'failed' && (
<button
onClick={() => handlePasarAEspera(proceso)}
disabled={changingStateId === proceso.task_id}
className={`group inline-flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200 ${
changingStateId === proceso.task_id
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'text-yellow-700 bg-yellow-100 hover:bg-yellow-200 hover:text-yellow-800'
}`}
title={changingStateId === proceso.task_id ? 'Reiniciando...' : 'Reintentar proceso'}
>
{changingStateId === proceso.task_id ? (
<svg className="animate-spin h-4 w-4" 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="h-4 w-4 transition-transform group-hover:scale-110" 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>
)}
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Paginación para Procesos */}
{procesosCount > 0 && (
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span className="text-sm text-gray-700">
<span className="hidden sm:inline">Mostrando </span>
<span className="font-medium">{((procesosPage - 1) * procesosPageSize) + 1}</span>-
<span className="font-medium">{Math.min(procesosPage * procesosPageSize, procesosCount)}</span>
<span className="hidden sm:inline"> de </span>
<span className="sm:hidden">/</span>
<span className="font-medium">{procesosCount}</span>
<span className="hidden sm:inline"> procesos</span>
</span>
<select
value={procesosPageSize}
onChange={(e) => {
setProcesosPageSize(Number(e.target.value));
setProcesosPage(1);
}}
className="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full 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 sm:justify-end space-x-1 sm:space-x-2">
<button
onClick={() => setProcesosPage(Math.max(1, procesosPage - 1))}
disabled={procesosPage === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Anterior</span>
<svg className="h-5 w-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 border border-gray-300 bg-white text-sm font-medium text-gray-700">
Página {procesosPage} de {Math.ceil(procesosCount / procesosPageSize)}
</span>
<button
onClick={() => setProcesosPage(Math.min(Math.ceil(procesosCount / procesosPageSize), procesosPage + 1))}
disabled={procesosPage >= Math.ceil(procesosCount / procesosPageSize)}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="sr-only">Siguiente</span>
<svg className="h-5 w-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 lg:flex-row lg:items-start lg:justify-between gap-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
Auditoría 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 w-fit">
Análisis Completo
</span>
</div>
{/* Botones de Auditoría */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 sm:gap-3 w-full lg:w-auto">
<button
onClick={() => handleAuditarPedimentoCompleto()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
>
<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="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">Auditar Pedimento Completo</span>
<span className="sm:hidden">Ped. Completo</span>
</button>
<button
onClick={() => handleAuditarEDocs()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
>
<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="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 className="hidden sm:inline">Auditar EDocs</span>
<span className="sm:hidden">EDocs</span>
</button>
<button
onClick={() => handleAuditarPartidas()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
>
<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="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">Auditar Partidas</span>
<span className="sm:hidden">Partidas</span>
</button>
<button
onClick={() => handleAuditarAcuses()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
>
<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="M5 13l4 4L19 7" />
</svg>
<span className="hidden sm:inline">Auditar Acuses</span>
<span className="sm:hidden">Acuses</span>
</button>
<button
onClick={() => handleAuditarCoves()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
>
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span className="hidden sm:inline">Auditar COVEs</span>
<span className="sm:hidden">COVEs</span>
</button>
<button
onClick={() => handleAuditarAcuseCoves()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
>
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="hidden sm:inline">Auditar Acuse COVEs</span>
<span className="sm:hidden">Ac. COVEs</span>
</button>
<button
onClick={() => handleCompararRemesasCoves()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 shadow-md hover:shadow-lg w-full sm:col-span-2 lg:col-span-1"
>
<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="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>
<span className="hidden sm:inline">Comparar Remesas vs COVEs</span>
<span className="sm:hidden">Comparar</span>
</button>
<button
onClick={() => handleVerificarServiciosCreados()}
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 shadow-md hover:shadow-lg w-full sm:col-span-2 lg:col-span-1"
>
<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="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 className="hidden sm:inline">Verificar Servicios Creados</span>
<span className="sm:hidden">Verificar</span>
</button>
</div>
</div>
</div>
{/* Contenido del Auditor */}
<div className="space-y-6">
{/* Resumen de Estado */}
<div className="bg-gradient-to-b from-white to-gray-50 rounded-xl p-6 border border-gray-200 shadow-lg">
<h4 className="text-lg font-semibold text-gray-900 mb-6 flex items-center border-b pb-4">
<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 lg:grid-cols-3 gap-6">
{/* Pedimentos */}
<div className="flex flex-col bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Pedimentos</span>
<div className="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center">
<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 justify-between items-center">
<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 bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">COVEs</span>
<div className="h-8 w-8 rounded-full bg-purple-100 flex items-center justify-center">
<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 justify-between items-center">
<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 bg-white rounded-lg p-4 border border-gray-200 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="h-8 w-8 rounded-full bg-indigo-100 flex items-center justify-center">
<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 justify-between items-center">
<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 bg-white rounded-lg p-4 border border-gray-200 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="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center">
<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 justify-between items-center">
<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 bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Acuses</span>
<div className="h-8 w-8 rounded-full bg-indigo-100 flex items-center justify-center">
<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 justify-between items-center">
<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 bg-white rounded-lg p-4 border border-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-600">Partidas</span>
<div className="h-8 w-8 rounded-full bg-orange-100 flex items-center justify-center">
<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 justify-between items-center">
<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 bg-white rounded-lg p-4 border border-gray-200 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="h-8 w-8 rounded-full bg-gray-100 flex items-center justify-center">
<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="text-center py-4">
<svg className="mx-auto h-12 w-12 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="bg-white rounded-xl p-6 shadow-lg border">
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<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 bg-gray-50 border border-gray-200 rounded-lg">
<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 md:grid-cols-2 gap-4">
{/* 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="text-sm text-gray-600 mt-1">
{`${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="space-y-1 mt-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="space-y-1 mt-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="text-sm text-gray-600 mt-1">
{`${dashboardSummary.partidas.partidas_descargadas} de ${dashboardSummary.partidas.total} partidas procesadas`}
</p>
</div>
</div>
</div>
</div>
{/* Recomendaciones */}
{(dashboardSummary.cumplimiento_total < 100) && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h6 className="font-medium text-blue-800 mb-2 flex items-center">
<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="bg-white rounded-xl p-6 shadow-lg border">
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<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="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4">
<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="text-sm text-gray-500 mt-2">Estamos preparando el historial de actividades</p>
<p className="text-sm text-blue-600 mt-4">Esta funcionalidad estará disponible próximamente</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
{/* Modal de vista previa mejorado */}
{previewOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<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 bg-gradient-to-r from-blue-600 to-blue-800 text-white">
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="bg-white/20 rounded-xl p-2 flex-shrink-0">
<svg className="h-6 w-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 gap-2 flex-shrink-0">
{/* Botón de descarga siempre visible */}
{previewDoc && (
<button
onClick={() => downloadDocument(previewDoc)}
className="p-2 bg-white/20 hover:bg-white/30 rounded-xl transition-colors duration-200 group"
title="Descargar documento"
>
<svg className="h-5 w-5 group-hover:scale-110 transition-transform" 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 bg-white/20 rounded-xl p-1">
<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="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 12H4" />
</svg>
</button>
<span className="text-xs font-medium px-2">{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="h-4 w-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="h-4 w-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 bg-white/20 hover:bg-red-500/80 rounded-xl transition-colors duration-200"
title="Cerrar vista previa"
>
<svg className="h-5 w-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-1 flex flex-col min-h-0 bg-gray-50">
{previewLoading ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="inline-flex items-center space-x-3 text-blue-600">
<svg className="animate-spin h-10 w-10" 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="text-gray-500 mt-2">Preparando vista previa</p>
</div>
</div>
) : previewError ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center text-red-600 max-w-md">
<div className="bg-red-50 rounded-full w-20 h-20 mx-auto mb-4 flex items-center justify-center">
<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="text-lg font-semibold mb-2">Error al cargar documento</h4>
<p className="text-sm text-gray-600">{previewError}</p>
<button
onClick={() => setPreviewError(null)}
className="mt-4 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg transition-colors"
>
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-1 flex items-center justify-center p-4 overflow-auto bg-gray-100">
<div className="relative">
<img
src={previewUrl}
alt="Vista previa"
className="max-w-none shadow-lg rounded-lg border-2 border-white"
style={{
transform: `scale(${imageZoom})`,
transformOrigin: 'center',
transition: 'transform 0.2s ease'
}}
/>
</div>
</div>
) : previewType === 'xml' ? (
<div className="flex-1 flex flex-col bg-white">
<div className="bg-gray-100 px-6 py-3 border-b border-gray-200 flex items-center justify-between">
<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 text-sm text-blue-600 hover:text-blue-800 px-4 py-2 rounded-lg border border-blue-300 bg-blue-50 hover:bg-blue-100 transition-colors"
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-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'
}}
dangerouslySetInnerHTML={{ __html: previewXmlHtml }}
/>
</div>
) : previewType === 'txt' ? (
<div className="flex-1 bg-white p-6 overflow-auto">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<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="text-sm text-gray-800 whitespace-pre-wrap font-mono leading-relaxed">
{previewContent || 'Contenido no disponible'}
</pre>
</div>
</div>
) : previewUrl ? (
<div className="flex-1 flex items-center justify-center p-8 bg-gray-50">
<div className="text-center">
<div className="bg-blue-50 rounded-full w-20 h-20 mx-auto mb-6 flex items-center justify-center">
<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="text-lg font-semibold text-gray-900 mb-2">Vista previa no disponible</h4>
<p className="text-gray-600 mb-6">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 space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl font-medium transition-colors shadow-lg 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-1 flex items-center justify-center 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 de confirmación para eliminación */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4 transform transition-all duration-300 scale-100">
{/* Header del modal */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center gap-3">
<div className="bg-red-100 rounded-full p-3">
<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="text-gray-700 mb-3">
¿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="bg-red-50 border border-red-200 rounded-lg p-3">
<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="text-sm text-red-700 mt-1">
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="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
<button
onClick={() => setShowDeleteModal(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 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 transition-colors duration-200"
>
Cancelar
</button>
<button
onClick={confirmDeleteDocuments}
className="px-4 py-2 text-sm font-medium text-white 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 transition-colors duration-200 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Eliminar {selectedDocuments.length} documento{selectedDocuments.length !== 1 ? 's' : ''}
</button>
</div>
</div>
</div>
)}
</div>
);
}