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