4124 lines
214 KiB
JavaScript
4124 lines
214 KiB
JavaScript
import React, { useEffect, useState, useRef } from 'react';
|
|
// Animación fade-in/slide-up para bloques
|
|
const fadeInSlideUp = `@keyframes fadein-slideup {
|
|
0% { opacity: 0; transform: translateY(40px); }
|
|
100% { opacity: 1; transform: translateY(0); }
|
|
}`;
|
|
|
|
if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-pedimento')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'fadein-slideup-pedimento';
|
|
style.innerHTML = fadeInSlideUp;
|
|
document.head.appendChild(style);
|
|
}
|
|
import hljs from 'highlight.js/lib/core';
|
|
import xml from 'highlight.js/lib/languages/xml';
|
|
import 'highlight.js/styles/github.css';
|
|
hljs.registerLanguage('xml', xml);
|
|
import { fetchPedimentoDocuments } from '../api/pedimentoDocuments';
|
|
import { fetchWithAuth, postWithAuth, putWithAuth } from '../fetchWithAuth';
|
|
import { fetchProcesamientoPedimentos } from '../api/procesos.ts';
|
|
import { fetchPedimentoCoves, downloadCove, downloadAcuseCove } from '../api/coves';
|
|
import { fetchPedimentoEdocuments, downloadEdocument, downloadAcuseEdocument } from '../api/edocuments';
|
|
import { useParams, Link } from 'react-router-dom';
|
|
import { useNotification } from '../context/NotificationContext';
|
|
|
|
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();
|
|
}
|
|
|
|
// Funci\u00f3n auxiliar para descargas individuales\nconst downloadFile = async (id, filename = 'archivo', showMessage) => {\n try {\n const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`);\n \n if (!res.ok) {\n showMessage('Error en la descarga del archivo', 'error');\n return;\n }\n \n const blob = await res.blob();\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n a.remove();\n window.URL.revokeObjectURL(url);\n } catch (error) {\n console.error('Error downloading file:', error);\n if (error.message === 'SESSION_EXPIRED') {\n showMessage('Tu sesi\u00f3n ha expirado, por favor inicia sesi\u00f3n de nuevo.', 'error');\n } else {\n showMessage('Error al descargar el archivo', 'error');\n }\n }\n};\n\n// Funci\u00f3n auxiliar para descargas\nconst downloadBulkZip = async (ids, showMessage, pedimentoName) => {\n try {\n const response = await fetchWithAuth(`${API_URL}/record/documents/bulk-download/`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ document_ids: ids })\n });\n \n if (!response.ok) throw new Error('Error en la descarga');\n \n const blob = await response.blob();\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `documentos_${pedimentoName || 'pedimento'}.zip`;\n document.body.appendChild(a);\n a.click();\n window.URL.revokeObjectURL(url);\n document.body.removeChild(a);\n \n showMessage('Descarga iniciada exitosamente', 'success');\n } catch (error) {\n showMessage('Error en la descarga: ' + error.message, 'error');\n }\n};
|
|
|
|
export default function PedimentoDetail() {
|
|
// Estados principales
|
|
const [activeTab, setActiveTab] = useState('documentos');
|
|
const [pedimento, setPedimento] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const { id } = useParams();
|
|
const { showMessage } = useNotification();
|
|
|
|
// Estados para documentos
|
|
const [documents, setDocuments] = useState([]);
|
|
const [docsCount, setDocsCount] = useState(0);
|
|
const [docsLoading, setDocsLoading] = useState(true);
|
|
const [docsError, setDocsError] = useState('');
|
|
const [docsNext, setDocsNext] = useState(null);
|
|
const [docsPrev, setDocsPrev] = useState(null);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
const [selected, setSelected] = useState([]);
|
|
const [downloading, setDownloading] = useState(false);
|
|
const [downloadingAll, setDownloadingAll] = useState(false);
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
|
|
// Filtros simplificados de documentos (nuevo diseño)
|
|
const [filters, setFilters] = useState({
|
|
name: '',
|
|
document_type: '',
|
|
extension: '',
|
|
date: '',
|
|
created_at__gte: '',
|
|
created_at__lte: '',
|
|
fuente: '',
|
|
pedimento_numero: ''
|
|
});
|
|
|
|
// Estados para filtros (legacy - para compatibilidad)
|
|
const [documentTypeFilter, setDocumentTypeFilter] = useState('');
|
|
const [fileNameFilter, setFileNameFilter] = useState('');
|
|
const [extensionFilter, setExtensionFilter] = useState('');
|
|
const [dateFilter, setDateFilter] = useState('');
|
|
const [orderBy, setOrderBy] = useState('');
|
|
const [orderDir, setOrderDir] = useState('asc');
|
|
|
|
// Estados para COVEs
|
|
const [coves, setCoves] = useState([]);
|
|
const [covesCount, setCovesCount] = useState(0);
|
|
const [covesLoading, setCovesLoading] = useState(false);
|
|
const [covesError, setCovesError] = useState('');
|
|
const [covesPage, setCovesPage] = useState(1);
|
|
const [covesPageSize, setCovesPageSize] = useState(10);
|
|
const [covesFilters, setCovesFilters] = useState({
|
|
numero_cove: '',
|
|
cove_descargado: '',
|
|
acuse_cove_descargado: '',
|
|
created_at__gte: '',
|
|
created_at__lte: ''
|
|
});
|
|
|
|
// Estados para EDocs
|
|
const [edocs, setEdocs] = useState([]);
|
|
const [edocsCount, setEdocsCount] = useState(0);
|
|
const [edocsLoading, setEdocsLoading] = useState(false);
|
|
const [edocsError, setEdocsError] = useState('');
|
|
const [edocsPage, setEdocsPage] = useState(1);
|
|
const [edocsPageSize, setEdocsPageSize] = useState(10);
|
|
const [edocsFilters, setEdocsFilters] = useState({
|
|
numero_edocument: '',
|
|
clave: '',
|
|
descripcion: '',
|
|
edocument_descargado: '',
|
|
acuse_descargado: '',
|
|
created_at__gte: '',
|
|
created_at__lte: ''
|
|
});
|
|
|
|
// Estados para Partidas
|
|
const [partidas, setPartidas] = useState([]);
|
|
const [partidasCount, setPartidasCount] = useState(0);
|
|
const [partidasLoading, setPartidasLoading] = useState(false);
|
|
const [partidasError, setPartidasError] = useState('');
|
|
const [partidasPage, setPartidasPage] = useState(1);
|
|
const [partidasPageSize, setPartidasPageSize] = useState(10);
|
|
const [partidasFilters, setPartidasFilters] = useState({
|
|
numero_partida: '',
|
|
descargado: '',
|
|
numero_partida__gte: '',
|
|
numero_partida__lte: '',
|
|
created_at__gte: '',
|
|
created_at__lte: ''
|
|
});
|
|
const [selectedPartidas, setSelectedPartidas] = useState([]);
|
|
const [downloadingPartidas, setDownloadingPartidas] = useState(false);
|
|
const [downloadingAllPartidas, setDownloadingAllPartidas] = useState(false);
|
|
|
|
// Estados para credenciales VUCEM
|
|
const [credenciales, setCredenciales] = useState([]);
|
|
const [processingPartida, setProcessingPartida] = useState(null);
|
|
const [processingCove, setProcessingCove] = useState(null);
|
|
const [processingAcuseCove, setProcessingAcuseCove] = useState(null);
|
|
const [processingEdoc, setProcessingEdoc] = useState(null);
|
|
const [processingAcuseEdoc, setProcessingAcuseEdoc] = useState(null);
|
|
|
|
// Función para obtener 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({});
|
|
|
|
// Estados para las acciones de procesos
|
|
const [executingId, setExecutingId] = useState(null);
|
|
const [changingStateId, setChangingStateId] = useState(null);
|
|
const [creatingService, setCreatingService] = useState(null);
|
|
|
|
// 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);
|
|
};
|
|
|
|
// 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 handleSelectAll = () => {
|
|
if (allSelected) setSelected([]);
|
|
else setSelected(allDocIds);
|
|
};
|
|
|
|
const handleBulkDownload = async (ids) => {
|
|
setDownloading(true);
|
|
await downloadBulkZip(ids, showMessage, pedimento?.pedimento);
|
|
setDownloading(false);
|
|
};
|
|
|
|
// Vista previa de documento
|
|
const handlePreview = async (doc) => {
|
|
setPreviewLoading(true);
|
|
setPreviewError('');
|
|
setPreviewUrl('');
|
|
setPreviewType('');
|
|
setPreviewXml('');
|
|
setPreviewDoc(doc);
|
|
setImageZoom(1);
|
|
setPreviewContent('');
|
|
setPreviewOpen(true);
|
|
try {
|
|
const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${doc.id}/`);
|
|
|
|
if (!res.ok) {
|
|
setPreviewError('Error al obtener el archivo');
|
|
setPreviewLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Detectar tipo de archivo
|
|
let type = '';
|
|
if (doc.extension) {
|
|
if (doc.extension.toLowerCase() === 'pdf') type = 'pdf';
|
|
else if (["jpg","jpeg","png","gif","bmp","webp"].includes(doc.extension.toLowerCase())) type = 'img';
|
|
else if (doc.extension.toLowerCase() === 'xml') type = 'xml';
|
|
else if (["txt","log","csv"].includes(doc.extension.toLowerCase())) type = 'txt';
|
|
else type = 'other';
|
|
}
|
|
setPreviewType(type);
|
|
|
|
if (type === 'xml') {
|
|
const text = await res.text();
|
|
const prettyText = formatXml(text);
|
|
setPreviewXml(prettyText);
|
|
// Formatear y resaltar XML
|
|
try {
|
|
const highlighted = hljs.highlight(prettyText, { language: 'xml' }).value;
|
|
setPreviewXmlHtml(highlighted);
|
|
} catch (e) {
|
|
setPreviewXmlHtml(prettyText);
|
|
}
|
|
setPreviewLoading(false);
|
|
} else if (type === 'txt') {
|
|
const text = await res.text();
|
|
setPreviewContent(text);
|
|
setPreviewLoading(false);
|
|
} else {
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
setPreviewUrl(url);
|
|
setPreviewLoading(false);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error in preview:', err);
|
|
if (err.message === 'SESSION_EXPIRED') {
|
|
setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.');
|
|
} else {
|
|
setPreviewError('Error al obtener el archivo');
|
|
}
|
|
setPreviewLoading(false);
|
|
}
|
|
};
|
|
|
|
// Cerrar modal y limpiar blob
|
|
const handleClosePreview = () => {
|
|
setPreviewOpen(false);
|
|
if (previewUrl) window.URL.revokeObjectURL(previewUrl);
|
|
setPreviewUrl('');
|
|
setPreviewType('');
|
|
setPreviewError('');
|
|
setPreviewXml('');
|
|
setPreviewXmlHtml('');
|
|
setPreviewDoc(null);
|
|
setPreviewContent('');
|
|
setImageZoom(1);
|
|
};
|
|
|
|
// Funciones para el nuevo diseño de documentos
|
|
const downloadAll = async () => {
|
|
setDownloadingAll(true);
|
|
try {
|
|
const allDocIds = documents.map(doc => doc.id);
|
|
await handleBulkDownload(allDocIds);
|
|
} catch (error) {
|
|
console.error('Error downloading all documents:', error);
|
|
showMessage('Error al descargar todos los documentos', 'error');
|
|
} finally {
|
|
setDownloadingAll(false);
|
|
}
|
|
};
|
|
|
|
const previewDocument = async (doc) => {
|
|
await handlePreview(doc);
|
|
};
|
|
|
|
const downloadDocument = async (doc) => {
|
|
const fileName = doc.archivo ? doc.archivo.split('/').pop() : `documento_${doc.id}`;
|
|
await downloadFile(doc.id, fileName, showMessage);
|
|
};
|
|
|
|
const formatFileSize = (bytes) => {
|
|
if (!bytes) return 'N/A';
|
|
const kb = bytes / 1024;
|
|
if (kb < 1024) {
|
|
return `${kb.toFixed(1)} KB`;
|
|
}
|
|
const mb = kb / 1024;
|
|
return `${mb.toFixed(1)} MB`;
|
|
};
|
|
|
|
const getFileExtension = (filename) => {
|
|
if (!filename) return '';
|
|
const parts = filename.split('.');
|
|
return parts.length > 1 ? parts[parts.length - 1] : '';
|
|
};
|
|
|
|
const getFuenteName = (fuente) => {
|
|
const fuentes = {
|
|
1: 'Manual',
|
|
2: 'VU',
|
|
3: 'Importación',
|
|
4: 'Sistema'
|
|
};
|
|
return fuentes[fuente] || 'Desconocida';
|
|
};
|
|
|
|
// Efecto para cargar datos del pedimento
|
|
useEffect(() => {
|
|
const fetchPedimento = async () => {
|
|
try {
|
|
const response = await fetchWithAuth(`${API_URL}/customs/pedimentos/${id}/`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setPedimento(data);
|
|
}
|
|
} catch (err) {
|
|
setError('Error al cargar el pedimento');
|
|
if (err.message === 'SESSION_EXPIRED') {
|
|
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (id) {
|
|
fetchPedimento();
|
|
}
|
|
}, [id, showMessage]);
|
|
|
|
// Fetch paginated documents
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
|
|
console.log('Starting to fetch documents with:', { id, page, pageSize, filters, orderBy, orderDir });
|
|
|
|
setDocsLoading(true);
|
|
setDocsError('');
|
|
|
|
// Construir parámetros de filtros usando el nuevo sistema
|
|
const apiFilters = {};
|
|
if (filters.document_type) apiFilters.document_type = filters.document_type;
|
|
if (filters.name) apiFilters.archivo__icontains = filters.name;
|
|
if (filters.extension) apiFilters.extension = filters.extension;
|
|
if (filters.date) apiFilters.created_at__date = filters.date;
|
|
if (filters.created_at__gte) apiFilters.created_at__gte = filters.created_at__gte;
|
|
if (filters.created_at__lte) apiFilters.created_at__lte = filters.created_at__lte;
|
|
if (filters.fuente) apiFilters.fuente = filters.fuente;
|
|
if (filters.pedimento_numero) apiFilters.pedimento_numero__icontains = filters.pedimento_numero;
|
|
|
|
// Mantener compatibilidad con filtros legacy si están en uso
|
|
if (documentTypeFilter) apiFilters.document_type = documentTypeFilter;
|
|
if (fileNameFilter) apiFilters.archivo__icontains = fileNameFilter;
|
|
if (extensionFilter) apiFilters.extension = extensionFilter;
|
|
if (dateFilter) apiFilters.created_at__date = dateFilter;
|
|
|
|
if (orderBy) {
|
|
const orderField = orderBy === 'archivo' ? 'archivo' :
|
|
orderBy === 'document_type' ? 'document_type' :
|
|
orderBy === 'created_at' ? 'created_at' :
|
|
orderBy === 'size' ? 'size' : orderBy;
|
|
apiFilters.ordering = orderDir === 'desc' ? `-${orderField}` : orderField;
|
|
}
|
|
|
|
console.log('Calling fetchPedimentoDocuments with filters:', apiFilters);
|
|
|
|
fetchPedimentoDocuments(id, page, pageSize, apiFilters)
|
|
.then((data) => {
|
|
console.log('Received documents data:', data);
|
|
setDocuments(data.results);
|
|
setDocsCount(data.count);
|
|
setDocsNext(data.next);
|
|
setDocsPrev(data.previous);
|
|
setDocsLoading(false);
|
|
})
|
|
.catch(err => {
|
|
console.error('Error fetching documents:', err);
|
|
if (err.message === 'SESSION_EXPIRED') {
|
|
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
|
} else {
|
|
setDocsError(err.message);
|
|
}
|
|
setDocsLoading(false);
|
|
});
|
|
}, [id, page, pageSize, filters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir, showMessage]);
|
|
|
|
// Resetear página cuando cambien los filtros
|
|
useEffect(() => {
|
|
setPage(1);
|
|
}, [filters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter]);
|
|
|
|
// Debug logs
|
|
useEffect(() => {
|
|
console.log('Documents data:', { documents, docsCount, page, pageSize, docsLoading, docsError });
|
|
}, [documents, docsCount, page, pageSize, docsLoading, docsError]);
|
|
|
|
// Fetch COVEs cuando sea necesario
|
|
useEffect(() => {
|
|
if (!id || activeTab !== 'coves') return;
|
|
|
|
setCovesLoading(true);
|
|
setCovesError('');
|
|
|
|
fetchPedimentoCoves(id, covesPage, covesPageSize, covesFilters)
|
|
.then((data) => {
|
|
setCoves(data.results);
|
|
setCovesCount(data.count);
|
|
setCovesLoading(false);
|
|
})
|
|
.catch(err => {
|
|
console.error('Error fetching COVEs:', err);
|
|
if (err.message === 'SESSION_EXPIRED') {
|
|
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
|
} else {
|
|
setCovesError(err.message);
|
|
}
|
|
setCovesLoading(false);
|
|
});
|
|
}, [id, activeTab, covesPage, covesPageSize, covesFilters, showMessage]);
|
|
|
|
// Resetear página de COVEs cuando cambien los filtros
|
|
useEffect(() => {
|
|
setCovesPage(1);
|
|
}, [covesFilters]);
|
|
|
|
// Fetch EDocs cuando sea necesario
|
|
useEffect(() => {
|
|
if (!id || activeTab !== 'edocs') return;
|
|
|
|
setEdocsLoading(true);
|
|
setEdocsError('');
|
|
|
|
fetchPedimentoEdocuments(id, edocsPage, edocsPageSize, edocsFilters)
|
|
.then((data) => {
|
|
setEdocs(data.results);
|
|
setEdocsCount(data.count);
|
|
setEdocsLoading(false);
|
|
})
|
|
.catch(err => {
|
|
console.error('Error fetching EDocs:', err);
|
|
if (err.message === 'SESSION_EXPIRED') {
|
|
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
|
} else {
|
|
setEdocsError(err.message);
|
|
}
|
|
setEdocsLoading(false);
|
|
});
|
|
}, [id, activeTab, edocsPage, edocsPageSize, edocsFilters, showMessage]);
|
|
|
|
// Resetear página de EDocs cuando cambien los filtros
|
|
useEffect(() => {
|
|
setEdocsPage(1);
|
|
}, [edocsFilters]);
|
|
|
|
// Funciones para acciones de Procesos
|
|
const updateProcesoEstado = (procId, nuevoEstado) => {
|
|
setProcesos(procesos =>
|
|
procesos.map(proc =>
|
|
proc.id === procId ? { ...proc, estado: 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}/customs/procesamientopedimentos/`, 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
|
|
};
|
|
fetchProcesamientoPedimentos(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, 2); // 2 = Procesando
|
|
|
|
try {
|
|
const body = {
|
|
estado: 1, // Cambiar a En Espera
|
|
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}/customs/procesamientopedimentos/${proc.id}/`, body);
|
|
|
|
if (!res.ok) {
|
|
// Si falla, revertir a estado Error
|
|
updateProcesoEstado(proc.id, 4); // 4 = Error
|
|
throw new Error('Error al cambiar el estado del proceso');
|
|
}
|
|
|
|
// Cambiar estado visual a "En Espera" si fue exitoso
|
|
updateProcesoEstado(proc.id, 1); // 1 = En Espera
|
|
|
|
showMessage('Estado cambiado a "En Espera" correctamente', 'success');
|
|
|
|
} catch (err) {
|
|
console.error('Error cambiando estado:', err);
|
|
updateProcesoEstado(proc.id, 4); // 4 = Error
|
|
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, 2); // 2 = Procesando
|
|
|
|
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.estado); // 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, 4); // 4 = Error
|
|
throw new Error('Error al ejecutar el servicio');
|
|
}
|
|
|
|
// Si es exitoso, cambiar estado a Finalizado
|
|
updateProcesoEstado(proc.id, 3); // 3 = Finalizado
|
|
|
|
showMessage('El servicio se ha ejecutado correctamente', 'success');
|
|
} catch (err) {
|
|
// Cambiar estado a Error en caso de excepción
|
|
updateProcesoEstado(proc.id, 4); // 4 = Error
|
|
|
|
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');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
|
|
setProcesosLoading(true);
|
|
setProcesosError('');
|
|
|
|
// Crear filtros incluyendo el pedimento
|
|
const filters = {
|
|
...procesosFilters,
|
|
pedimento: id // Filtrar por el pedimento actual
|
|
};
|
|
|
|
fetchProcesamientoPedimentos(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, showMessage]);
|
|
|
|
// Resetear página de Procesos cuando cambie el pedimento
|
|
useEffect(() => {
|
|
setProcesosPage(1);
|
|
}, [id]);
|
|
|
|
// Funciones para COVEs
|
|
const handleCoveDownload = async (cove) => {
|
|
try {
|
|
await downloadCove(cove.id);
|
|
showMessage(`COVE ${cove.numero_cove} descargado exitosamente`, 'success');
|
|
} catch (error) {
|
|
showMessage(`Error al descargar COVE: ${error.message}`, 'error');
|
|
}
|
|
};
|
|
|
|
const handleAcuseCoveDownload = async (cove) => {
|
|
try {
|
|
await downloadAcuseCove(cove.id);
|
|
showMessage(`Acuse de COVE ${cove.numero_cove} descargado exitosamente`, 'success');
|
|
} catch (error) {
|
|
showMessage(`Error al descargar acuse de COVE: ${error.message}`, 'error');
|
|
}
|
|
};
|
|
|
|
// Funciones para EDocs
|
|
const handleEdocDownload = async (edoc) => {
|
|
try {
|
|
await downloadEdocument(edoc.id);
|
|
showMessage(`EDocs ${edoc.numero_edocument} descargado exitosamente`, 'success');
|
|
} catch (error) {
|
|
showMessage(`Error al descargar EDocs: ${error.message}`, 'error');
|
|
}
|
|
};
|
|
|
|
const handleAcuseEdocDownload = async (edoc) => {
|
|
try {
|
|
await downloadAcuseEdocument(edoc.id);
|
|
showMessage(`Acuse de EDocs ${edoc.numero_edocument} descargado exitosamente`, 'success');
|
|
} catch (error) {
|
|
showMessage(`Error al descargar acuse de EDocs: ${error.message}`, 'error');
|
|
}
|
|
};
|
|
|
|
// Funciones para procesar peticiones
|
|
const handleCoveRequest = async (cove) => {
|
|
console.log('Request cove:', cove);
|
|
showMessage(`Procesando petición para COVE #${cove.numero_cove}...`, 'info');
|
|
// Aquí implementarías la lógica de petición específica para COVEs
|
|
};
|
|
|
|
// Función para procesar COVE
|
|
const handleCoveProcess = async (cove) => {
|
|
setProcessingCove(cove.id);
|
|
|
|
try {
|
|
// Obtener credenciales para el contribuyente del pedimento
|
|
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
|
|
|
|
if (credencialesList.length === 0) {
|
|
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
|
|
return;
|
|
}
|
|
|
|
// Usar la primera credencial activa disponible
|
|
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
|
|
|
|
const requestBody = {
|
|
cove: {
|
|
id: cove.id,
|
|
cove: cove.numero_cove
|
|
},
|
|
pedimento: {
|
|
id: pedimento.id,
|
|
pedimento: pedimento.pedimento,
|
|
pedimento_app: pedimento.pedimento_app,
|
|
aduana: pedimento.aduana,
|
|
patente: pedimento.patente,
|
|
organizacion: pedimento.organizacion,
|
|
regimen: pedimento.regimen || "test",
|
|
clave_pedimento: pedimento.clave_pedimento || "test",
|
|
numero_operacion: pedimento.numero_operacion || "test"
|
|
},
|
|
credencial: {
|
|
id: credencial.id,
|
|
user: credencial.usuario,
|
|
password: credencial.password,
|
|
efirma: credencial.efirma,
|
|
key: credencial.key,
|
|
cer: credencial.cer,
|
|
is_active: credencial.is_active,
|
|
organizacion: credencial.organizacion
|
|
}
|
|
};
|
|
|
|
// Verificar si MICROSERVICE_URL_2 está definido
|
|
if (!MICROSERVICE_URL_2) {
|
|
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
|
|
}
|
|
|
|
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/cove/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showMessage('COVE procesado correctamente', 'success');
|
|
} else {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || 'Error al procesar el COVE');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error procesando COVE:', error);
|
|
showMessage(`Error al procesar el COVE: ${error.message}`, 'error');
|
|
} finally {
|
|
setProcessingCove(null);
|
|
}
|
|
};
|
|
|
|
// Función para procesar Acuse de COVE
|
|
const handleAcuseCoveProcess = async (cove) => {
|
|
setProcessingAcuseCove(cove.id);
|
|
|
|
try {
|
|
// Obtener credenciales para el contribuyente del pedimento
|
|
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
|
|
|
|
if (credencialesList.length === 0) {
|
|
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
|
|
return;
|
|
}
|
|
|
|
// Usar la primera credencial activa disponible
|
|
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
|
|
|
|
const requestBody = {
|
|
cove: {
|
|
id: cove.id,
|
|
cove: cove.numero_cove
|
|
},
|
|
pedimento: {
|
|
id: pedimento.id,
|
|
pedimento: pedimento.pedimento,
|
|
pedimento_app: pedimento.pedimento_app,
|
|
aduana: pedimento.aduana,
|
|
patente: pedimento.patente,
|
|
organizacion: pedimento.organizacion,
|
|
regimen: pedimento.regimen || "test",
|
|
clave_pedimento: pedimento.clave_pedimento || "test",
|
|
numero_operacion: pedimento.numero_operacion || "test"
|
|
},
|
|
credencial: {
|
|
id: credencial.id,
|
|
user: credencial.usuario,
|
|
password: credencial.password,
|
|
efirma: credencial.efirma,
|
|
key: credencial.key,
|
|
cer: credencial.cer,
|
|
is_active: credencial.is_active,
|
|
organizacion: credencial.organizacion
|
|
}
|
|
};
|
|
|
|
// Verificar si MICROSERVICE_URL_2 está definido
|
|
if (!MICROSERVICE_URL_2) {
|
|
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
|
|
}
|
|
|
|
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/acuse/cove/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showMessage('Acuse de COVE procesado correctamente', 'success');
|
|
} else {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || 'Error al procesar el acuse de COVE');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error procesando acuse de COVE:', error);
|
|
showMessage(`Error al procesar el acuse de COVE: ${error.message}`, 'error');
|
|
} finally {
|
|
setProcessingAcuseCove(null);
|
|
}
|
|
};
|
|
|
|
// Función para procesar EDoc
|
|
const handleEdocProcess = async (edoc) => {
|
|
setProcessingEdoc(edoc.id);
|
|
|
|
try {
|
|
// Obtener credenciales para el contribuyente del pedimento
|
|
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
|
|
|
|
if (credencialesList.length === 0) {
|
|
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
|
|
return;
|
|
}
|
|
|
|
// Usar la primera credencial activa disponible
|
|
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
|
|
|
|
const requestBody = {
|
|
edoc: {
|
|
id: edoc.id,
|
|
numero_edocument: edoc.numero_edocument
|
|
},
|
|
idEDocument: edoc.numero_edocument,
|
|
pedimento: {
|
|
id: pedimento.id,
|
|
pedimento: pedimento.pedimento,
|
|
pedimento_app: pedimento.pedimento_app,
|
|
aduana: pedimento.aduana,
|
|
patente: pedimento.patente,
|
|
organizacion: pedimento.organizacion,
|
|
regimen: pedimento.regimen || "test",
|
|
clave_pedimento: pedimento.clave_pedimento || "test",
|
|
numero_operacion: pedimento.numero_operacion || "test"
|
|
},
|
|
credencial: {
|
|
id: credencial.id,
|
|
user: credencial.usuario,
|
|
password: credencial.password,
|
|
efirma: credencial.efirma,
|
|
key: credencial.key,
|
|
cer: credencial.cer,
|
|
is_active: credencial.is_active,
|
|
organizacion: credencial.organizacion
|
|
}
|
|
};
|
|
|
|
// Verificar si MICROSERVICE_URL_2 está definido
|
|
if (!MICROSERVICE_URL_2) {
|
|
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
|
|
}
|
|
|
|
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/download/edoc/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showMessage('EDoc procesado correctamente', 'success');
|
|
} else {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || 'Error al procesar el EDoc');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error procesando EDoc:', error);
|
|
showMessage(`Error al procesar el EDoc: ${error.message}`, 'error');
|
|
} finally {
|
|
setProcessingEdoc(null);
|
|
}
|
|
};
|
|
|
|
// Función para procesar Acuse de EDoc
|
|
const handleAcuseEdocProcess = async (edoc) => {
|
|
setProcessingAcuseEdoc(edoc.id);
|
|
|
|
try {
|
|
// Obtener credenciales para el contribuyente del pedimento
|
|
const credencialesList = await fetchCredenciales(pedimento.contribuyente);
|
|
|
|
if (credencialesList.length === 0) {
|
|
showMessage('No se encontraron credenciales VUCEM para este contribuyente', 'error');
|
|
return;
|
|
}
|
|
|
|
// Usar la primera credencial activa disponible
|
|
const credencial = credencialesList.find(c => c.is_active) || credencialesList[0];
|
|
|
|
const requestBody = {
|
|
edoc: {
|
|
id: edoc.id,
|
|
numero_edocument: edoc.numero_edocument
|
|
},
|
|
idEDocument: edoc.numero_edocument,
|
|
pedimento: {
|
|
id: pedimento.id,
|
|
pedimento: pedimento.pedimento,
|
|
pedimento_app: pedimento.pedimento_app,
|
|
aduana: pedimento.aduana,
|
|
patente: pedimento.patente,
|
|
organizacion: pedimento.organizacion,
|
|
regimen: pedimento.regimen || "test",
|
|
clave_pedimento: pedimento.clave_pedimento || "test",
|
|
numero_operacion: pedimento.numero_operacion || "test"
|
|
},
|
|
credencial: {
|
|
id: credencial.id,
|
|
user: credencial.usuario,
|
|
password: credencial.password,
|
|
efirma: credencial.efirma,
|
|
key: credencial.key,
|
|
cer: credencial.cer,
|
|
is_active: credencial.is_active,
|
|
organizacion: credencial.organizacion
|
|
}
|
|
};
|
|
|
|
// Verificar si MICROSERVICE_URL_2 está definido
|
|
if (!MICROSERVICE_URL_2) {
|
|
throw new Error('La variable de entorno VITE_EFC_MICROSERVICE_URL_2 no está configurada');
|
|
}
|
|
|
|
const response = await fetchWithAuth(`${MICROSERVICE_URL_2}/services/acuse/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showMessage('Acuse de EDoc procesado correctamente', 'success');
|
|
} else {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || 'Error al procesar el acuse de EDoc');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error procesando acuse de EDoc:', error);
|
|
showMessage(`Error al procesar el acuse de EDoc: ${error.message}`, 'error');
|
|
} finally {
|
|
setProcessingAcuseEdoc(null);
|
|
}
|
|
};
|
|
|
|
const handleEdocRequest = async (edoc) => {
|
|
console.log('Request edoc:', edoc);
|
|
showMessage(`Procesando petición para EDocs #${edoc.numero_edocument}...`, 'info');
|
|
// Aquí implementarías la lógica de petición específica para EDocs
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return 'N/A';
|
|
return new Date(dateString).toLocaleDateString('es-ES', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
// Funciones para Partidas
|
|
const handlePartidaPreview = async (partida) => {
|
|
// Pasar un objeto que simule un documento con el ID del pedimento
|
|
const partidaDoc = { ...partida, id: pedimento.id };
|
|
await handlePreview(partidaDoc);
|
|
};
|
|
|
|
const handlePartidaDownload = async (partida) => {
|
|
const fileName = partida.archivo ? partida.archivo.split('/').pop() : `partida_${partida.numero_partida}_${partida.id}`;
|
|
await downloadFile(pedimento.id, fileName, showMessage);
|
|
};
|
|
|
|
const handlePartidaRequest = async (partida) => {
|
|
await handlePartidaProcess(partida);
|
|
};
|
|
|
|
// Funciones de selección para partidas
|
|
const allPartidaIds = partidas.map(partida => partida.id);
|
|
const allPartidasSelected = selectedPartidas.length === allPartidaIds.length && allPartidaIds.length > 0;
|
|
|
|
const handleSelectPartida = (id) => {
|
|
setSelectedPartidas(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
|
|
};
|
|
|
|
const handleSelectAllPartidas = () => {
|
|
if (allPartidasSelected) setSelectedPartidas([]);
|
|
else setSelectedPartidas(allPartidaIds);
|
|
};
|
|
|
|
const handleBulkDownloadPartidas = async (ids) => {
|
|
setDownloadingPartidas(true);
|
|
await downloadBulkZip(ids, showMessage, `${pedimento?.pedimento}_partidas`);
|
|
setDownloadingPartidas(false);
|
|
};
|
|
|
|
const downloadAllPartidas = async () => {
|
|
setDownloadingAllPartidas(true);
|
|
try {
|
|
const allPartidaIds = partidas.map(partida => partida.id);
|
|
await handleBulkDownloadPartidas(allPartidaIds);
|
|
} catch (error) {
|
|
console.error('Error downloading all partidas:', error);
|
|
showMessage('Error al descargar todas las partidas', 'error');
|
|
} finally {
|
|
setDownloadingAllPartidas(false);
|
|
}
|
|
};
|
|
|
|
// Estados de carga
|
|
if (loading) return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600 text-lg">Cargando detalle del pedimento...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (error) return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="bg-red-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
|
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">Error al cargar</h2>
|
|
<p className="text-gray-600">{error}</p>
|
|
<Link to="/expedientes" className="mt-4 inline-flex items-center text-blue-600 hover:text-blue-800">
|
|
← Volver a expedientes
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6">
|
|
<div className="max-w-7xl mx-auto">
|
|
|
|
{/* Header */}
|
|
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 mb-6 animate-fadein-slideup opacity-0"
|
|
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
|
<div className="p-6">
|
|
<Link
|
|
to="/expedientes"
|
|
className="inline-flex items-center text-blue-600 hover:text-blue-800 transition-colors duration-200 mb-2"
|
|
>
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7"></path>
|
|
</svg>
|
|
<span className="font-medium">Volver a la lista</span>
|
|
</Link>
|
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 mb-1">
|
|
Detalle de Pedimento
|
|
</h1>
|
|
<p className="text-gray-600">
|
|
Información completa del pedimento y documentos asociados
|
|
</p>
|
|
{docsCount > 0 && (
|
|
<div className="mt-2">
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
|
📄 {docsCount} documentos
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Información del Pedimento */}
|
|
{pedimento && (
|
|
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 mb-6 animate-fadein-slideup opacity-0"
|
|
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
|
|
<div className="px-6 py-4 border-b border-gray-200/50">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl">
|
|
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-xl font-bold text-gray-900">Información General</h2>
|
|
</div>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100/50 p-4 rounded-xl border border-blue-200/50 shadow-sm">
|
|
<dt className="text-sm font-semibold text-blue-700 mb-2 flex items-center">
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
</svg>
|
|
Pedimento
|
|
</dt>
|
|
<dd className="text-xl font-bold text-gray-900">{pedimento.pedimento_app}</dd>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-green-50 to-green-100/50 p-4 rounded-xl border border-green-200/50 shadow-sm">
|
|
<dt className="text-sm font-semibold text-green-700 mb-2 flex items-center">
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
Contribuyente
|
|
</dt>
|
|
<dd className="text-xl font-bold text-gray-900">{pedimento.contribuyente}</dd>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-purple-50 to-purple-100/50 p-4 rounded-xl border border-purple-200/50 shadow-sm">
|
|
<dt className="text-sm font-semibold text-purple-700 mb-2 flex items-center">
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3a2 2 0 012-2h4a2 2 0 012 2v4m-6 0h6M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V9a2 2 0 00-2-2h-2" />
|
|
</svg>
|
|
Fecha de Pago
|
|
</dt>
|
|
<dd className="text-xl font-bold text-gray-900">{pedimento.fecha_pago}</dd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Sistema de pestañas */}
|
|
<div className="bg-white/80 backdrop-blur-xl shadow-xl rounded-2xl border border-blue-100/50 animate-fadein-slideup opacity-0"
|
|
style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards' }}>
|
|
|
|
{/* Navegación de pestañas */}
|
|
<div className="border-b border-gray-200/50 bg-gradient-to-r from-gray-50 to-blue-50/30">
|
|
<nav className="flex space-x-2 sm:space-x-4 lg:space-x-8 px-3 sm:px-6 overflow-x-auto scrollbar-hide" aria-label="Pestañas">
|
|
<button
|
|
onClick={() => handleTabChange('documentos')}
|
|
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
|
|
activeTab === 'documentos'
|
|
? 'border-blue-600 text-blue-600 bg-blue-50/50'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Documentos</span>
|
|
<span className="sm:hidden">Docs</span>
|
|
{activeTab === 'documentos' && docsCount > 0 && (
|
|
<span className="bg-blue-100 text-blue-600 text-xs font-semibold px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full">
|
|
{docsCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleTabChange('partidas')}
|
|
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
|
|
activeTab === 'partidas'
|
|
? 'border-blue-600 text-blue-600 bg-blue-50/50'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
|
</svg>
|
|
<span>Partidas</span>
|
|
{activeTab === 'partidas' && partidasCount > 0 && (
|
|
<span className="bg-purple-100 text-purple-600 text-xs font-semibold px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full">
|
|
{partidasCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleTabChange('coves')}
|
|
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
|
|
activeTab === 'coves'
|
|
? 'border-blue-600 text-blue-600 bg-blue-50/50'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
|
|
</svg>
|
|
<span className="hidden sm:inline">COVEs</span>
|
|
<span className="sm:hidden">COVEs</span>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleTabChange('edocs')}
|
|
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
|
|
activeTab === 'edocs'
|
|
? 'border-blue-600 text-blue-600 bg-blue-50/50'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>EDocs</span>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleTabChange('procesos')}
|
|
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
|
|
activeTab === 'procesos'
|
|
? 'border-blue-600 text-blue-600 bg-blue-50/50'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Procesos</span>
|
|
<span className="sm:hidden">Proc.</span>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleTabChange('auditor')}
|
|
className={`whitespace-nowrap py-3 sm:py-4 px-2 sm:px-3 lg:px-1 border-b-2 font-medium text-xs sm:text-sm transition-all duration-200 ${
|
|
activeTab === 'auditor'
|
|
? 'border-blue-600 text-blue-600 bg-blue-50/50'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-1 sm:space-x-2">
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditor de Pedimento</span>
|
|
<span className="sm:hidden">Auditor</span>
|
|
</div>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Contenido de las pestañas */}
|
|
{activeTab === 'documentos' && (
|
|
<div className="p-6">
|
|
{/* Header de la sección */}
|
|
<div className="mb-4 sm:mb-6 space-y-3 sm:space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
|
Documentos del Pedimento
|
|
</h3>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 w-fit">
|
|
{docsCount} documentos
|
|
</span>
|
|
</div>
|
|
|
|
{documents.length > 0 && (
|
|
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
|
|
<button
|
|
onClick={downloadAll}
|
|
disabled={downloadingAll}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
>
|
|
{downloadingAll ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span className="hidden sm:inline">Descargando...</span>
|
|
<span className="sm:hidden">Descargando...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Descargar Todos</span>
|
|
<span className="sm:hidden">Descargar</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">{showFilters ? 'Ocultar Filtros' : 'Mostrar Filtros'}</span>
|
|
<span className="sm:hidden">Filtros</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Filtros expandibles */}
|
|
{showFilters && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg border">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Buscar por nombre
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={filters.name}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, name: e.target.value }))}
|
|
placeholder="Nombre del archivo..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tipo de documento
|
|
</label>
|
|
<select
|
|
value={filters.document_type}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, document_type: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todos los tipos</option>
|
|
<option value="1">Partida</option>
|
|
<option value="2">Pedimento Completo</option>
|
|
<option value="3">Remesa</option>
|
|
<option value="4">Acuse EDocument</option>
|
|
<option value="5">EDocument</option>
|
|
<option value="7">Acuse Cove</option>
|
|
<option value="8">Cove</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Extensión
|
|
</label>
|
|
<select
|
|
value={filters.extension}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, extension: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todas las extensiones</option>
|
|
<option value="pdf">PDF</option>
|
|
<option value="xml">XML</option>
|
|
<option value="jpg">JPG</option>
|
|
<option value="png">PNG</option>
|
|
<option value="doc">DOC</option>
|
|
<option value="docx">DOCX</option>
|
|
<option value="xls">XLS</option>
|
|
<option value="xlsx">XLSX</option>
|
|
<option value="txt">TXT</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fuente
|
|
</label>
|
|
<select
|
|
value={filters.fuente || ''}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, fuente: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todas las fuentes</option>
|
|
<option value="1">Manual</option>
|
|
<option value="2">VU</option>
|
|
<option value="3">Importación</option>
|
|
<option value="4">Sistema</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Número de pedimento
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={filters.pedimento_numero || ''}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, pedimento_numero: e.target.value }))}
|
|
placeholder="Número de pedimento..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fecha de creación
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={filters.date}
|
|
onChange={(e) => setFilters(prev => ({ ...prev, date: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-end">
|
|
<button
|
|
onClick={() => setFilters({
|
|
name: '',
|
|
document_type: '',
|
|
extension: '',
|
|
date: '',
|
|
fuente: '',
|
|
pedimento_numero: ''
|
|
})}
|
|
className="w-full px-3 py-2 text-sm font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
|
>
|
|
Limpiar filtros
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{loading ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span className="ml-2 text-gray-600">Cargando documentos...</span>
|
|
</div>
|
|
) : error ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-red-600 mb-2">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">Error al cargar documentos</h3>
|
|
<p className="text-gray-600">{error}</p>
|
|
</div>
|
|
) : documents.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay documentos</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
No se encontraron documentos para este pedimento.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Tabla de documentos */}
|
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-300">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
<button
|
|
onClick={() => handleSort('archivo')}
|
|
className="group inline-flex items-center space-x-1 hover:text-gray-900"
|
|
>
|
|
<span>Documento</span>
|
|
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
|
</svg>
|
|
</button>
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
<button
|
|
onClick={() => handleSort('document_type')}
|
|
className="group inline-flex items-center space-x-1 hover:text-gray-900"
|
|
>
|
|
<span>Tipo</span>
|
|
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
|
</svg>
|
|
</button>
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
<button
|
|
onClick={() => handleSort('extension')}
|
|
className="group inline-flex items-center space-x-1 hover:text-gray-900"
|
|
>
|
|
<span>Extensión</span>
|
|
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
|
</svg>
|
|
</button>
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
<button
|
|
onClick={() => handleSort('size')}
|
|
className="group inline-flex items-center space-x-1 hover:text-gray-900"
|
|
>
|
|
<span>Tamaño</span>
|
|
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
|
</svg>
|
|
</button>
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Fuente
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
<button
|
|
onClick={() => handleSort('created_at')}
|
|
className="group inline-flex items-center space-x-1 hover:text-gray-900"
|
|
>
|
|
<span>Fecha de Creación</span>
|
|
<svg className="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
|
</svg>
|
|
</button>
|
|
</th>
|
|
<th scope="col" className="relative px-6 py-3">
|
|
<span className="sr-only">Acciones</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{documents.map((doc, index) => (
|
|
<tr key={`${doc.id}-${index}`} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900" title={doc.archivo}>
|
|
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
|
|
</div>
|
|
<div className="text-sm text-gray-500">
|
|
Pedimento: {doc.pedimento_numero || 'N/A'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
{getDocumentTypeName(doc.document_type)}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
{doc.extension ? doc.extension.toUpperCase() : 'N/A'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatFileSize(doc.size)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
{getFuenteName(doc.fuente)}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(doc.created_at)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => previewDocument(doc)}
|
|
className="text-blue-600 hover:text-blue-900 p-1 rounded"
|
|
title="Vista previa"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => downloadDocument(doc)}
|
|
className="text-green-600 hover:text-green-900 p-1 rounded"
|
|
title="Descargar"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paginación para documentos */}
|
|
{docsCount > 0 && (
|
|
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<span className="text-sm text-gray-700">
|
|
<span className="hidden sm:inline">Mostrando </span>
|
|
<span className="font-medium">{((page - 1) * pageSize) + 1}</span>-
|
|
<span className="font-medium">{Math.min(page * pageSize, docsCount)}</span>
|
|
<span className="hidden sm:inline">de </span>
|
|
<span className="sm:hidden">/</span>
|
|
<span className="font-medium">{docsCount}</span>
|
|
<span className="hidden sm:inline"> documentos</span>
|
|
</span>
|
|
<select
|
|
value={pageSize}
|
|
onChange={(e) => {
|
|
setPageSize(Number(e.target.value));
|
|
setPage(1);
|
|
}}
|
|
className="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto"
|
|
>
|
|
<option value={10}>10 por página</option>
|
|
<option value={25}>25 por página</option>
|
|
<option value={50}>50 por página</option>
|
|
<option value={100}>100 por página</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center sm:justify-end space-x-1 sm:space-x-2">
|
|
<button
|
|
onClick={() => setPage(Math.max(1, page - 1))}
|
|
disabled={page === 1}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Anterior</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
|
Página {page} de {Math.ceil(docsCount / pageSize)}
|
|
</span>
|
|
|
|
<button
|
|
onClick={() => setPage(Math.min(Math.ceil(docsCount / pageSize), page + 1))}
|
|
disabled={page >= Math.ceil(docsCount / pageSize)}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Siguiente</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'partidas' && (
|
|
<div className="p-6">
|
|
{/* Header de la sección */}
|
|
<div className="mb-4 sm:mb-6 space-y-3 sm:space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
|
Partidas del Pedimento
|
|
</h3>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 w-fit">
|
|
{partidasCount} partidas
|
|
</span>
|
|
</div>
|
|
|
|
{/* Botones de acción masiva */}
|
|
<div className="flex items-center space-x-3">
|
|
{selectedPartidas.length > 0 && (
|
|
<button
|
|
onClick={() => handleBulkDownloadPartidas(selectedPartidas)}
|
|
disabled={downloadingPartidas}
|
|
className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
>
|
|
{downloadingPartidas ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Descargando...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
Descargar seleccionadas ({selectedPartidas.length})
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filtros */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-7 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Número de partida
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={partidasFilters.numero_partida}
|
|
onChange={(e) => setPartidasFilters(prev => ({ ...prev, numero_partida: e.target.value }))}
|
|
placeholder="Número de partida..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Desde partida #
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max={partidasCount || 1}
|
|
value={partidasFilters.numero_partida__gte || ''}
|
|
onChange={(e) => setPartidasFilters(prev => ({ ...prev, numero_partida__gte: e.target.value }))}
|
|
placeholder="1"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Hasta partida #
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max={partidasCount || 1}
|
|
value={partidasFilters.numero_partida__lte || ''}
|
|
onChange={(e) => setPartidasFilters(prev => ({ ...prev, numero_partida__lte: e.target.value }))}
|
|
placeholder={partidasCount?.toString() || "1"}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Estado de descarga
|
|
</label>
|
|
<select
|
|
value={partidasFilters.descargado}
|
|
onChange={(e) => setPartidasFilters(prev => ({ ...prev, descargado: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="true">Descargado</option>
|
|
<option value="false">No descargado</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fecha desde
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={partidasFilters.created_at__gte || ''}
|
|
onChange={(e) => setPartidasFilters(prev => ({ ...prev, created_at__gte: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fecha hasta
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={partidasFilters.created_at__lte || ''}
|
|
onChange={(e) => setPartidasFilters(prev => ({ ...prev, created_at__lte: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-end">
|
|
<button
|
|
onClick={() => setPartidasFilters({
|
|
numero_partida: '',
|
|
descargado: '',
|
|
numero_partida__gte: '',
|
|
numero_partida__lte: '',
|
|
created_at__gte: '',
|
|
created_at__lte: ''
|
|
})}
|
|
className="w-full px-3 py-2 text-sm font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
|
>
|
|
Limpiar filtros
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{partidasLoading ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span className="ml-2 text-gray-600">Cargando partidas...</span>
|
|
</div>
|
|
) : partidasError ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-red-600 mb-2">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">Error al cargar partidas</h3>
|
|
<p className="text-gray-600">{partidasError}</p>
|
|
</div>
|
|
) : !partidas || partidas.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay partidas</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
No se encontraron partidas para este pedimento.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Tabla de partidas */}
|
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-300">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="relative px-6 py-3">
|
|
<input
|
|
type="checkbox"
|
|
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
checked={allPartidasSelected}
|
|
onChange={handleSelectAllPartidas}
|
|
/>
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
ID
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Número de Partida
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estado de Descarga
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Fecha de Creación
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Última Actualización
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Acciones
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{(partidas || []).map((partida, index) => (
|
|
<tr key={`${partida.id}-${index}`} className="hover:bg-gray-50">
|
|
<td className="relative px-6 py-4 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
checked={selectedPartidas.includes(partida.id)}
|
|
onChange={() => handleSelectPartida(partida.id)}
|
|
/>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
|
<svg className="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
#{partida.id}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{partida.numero_partida}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
partida.descargado
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{partida.descargado ? 'Descargado' : 'Pendiente'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{partida.created_at ? formatDate(partida.created_at) : 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{partida.updated_at ? formatDate(partida.updated_at) : 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div className="flex items-center justify-end space-x-2">
|
|
{/* Botón Petición (solo activo si está pendiente) */}
|
|
<button
|
|
onClick={() => handlePartidaRequest(partida)}
|
|
disabled={partida.descargado || processingPartida === partida.id}
|
|
className={`p-1 rounded transition-colors ${
|
|
partida.descargado || processingPartida === partida.id
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: 'text-purple-600 hover:text-purple-900'
|
|
}`}
|
|
title={
|
|
processingPartida === partida.id ? 'Procesando partida...' :
|
|
partida.descargado ? 'No disponible - Ya descargado' :
|
|
'Procesar petición'
|
|
}
|
|
>
|
|
{processingPartida === partida.id ? (
|
|
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 4H6a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-2m-4-1v8m0 0l3-3m-3 3L9 8m-5 5h2.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H20" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paginación para partidas */}
|
|
{partidasCount > 0 && (
|
|
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<span className="text-sm text-gray-700">
|
|
<span className="hidden sm:inline">Mostrando </span>
|
|
<span className="font-medium">{((partidasPage - 1) * partidasPageSize) + 1}</span>-
|
|
<span className="font-medium">{Math.min(partidasPage * partidasPageSize, partidasCount)}</span>
|
|
<span className="hidden sm:inline"> de </span>
|
|
<span className="sm:hidden">/</span>
|
|
<span className="font-medium">{partidasCount}</span>
|
|
<span className="hidden sm:inline"> partidas</span>
|
|
</span>
|
|
<select
|
|
value={partidasPageSize}
|
|
onChange={(e) => {
|
|
setPartidasPageSize(Number(e.target.value));
|
|
setPartidasPage(1);
|
|
}}
|
|
className="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto"
|
|
>
|
|
<option value={10}>10 por página</option>
|
|
<option value={25}>25 por página</option>
|
|
<option value={50}>50 por página</option>
|
|
<option value={100}>100 por página</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center sm:justify-end space-x-1 sm:space-x-2">
|
|
<button
|
|
onClick={() => setPartidasPage(Math.max(1, partidasPage - 1))}
|
|
disabled={partidasPage === 1}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Anterior</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
|
Página {partidasPage} de {Math.ceil((partidasCount || 0) / (partidasPageSize || 10)) || 1}
|
|
</span>
|
|
|
|
<button
|
|
onClick={() => setPartidasPage(Math.min(Math.ceil((partidasCount || 0) / (partidasPageSize || 10)) || 1, partidasPage + 1))}
|
|
disabled={partidasPage >= (Math.ceil((partidasCount || 0) / (partidasPageSize || 10)) || 1)}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Siguiente</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'coves' && (
|
|
<div className="p-6">
|
|
{/* Header de la sección */}
|
|
<div className="mb-4 sm:mb-6 space-y-3 sm:space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
|
COVEs del Pedimento
|
|
</h3>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 w-fit">
|
|
{covesCount} COVEs
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filtros */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg border">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Buscar por número COVE
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={covesFilters.numero_cove}
|
|
onChange={(e) => setCovesFilters(prev => ({ ...prev, numero_cove: e.target.value }))}
|
|
placeholder="Número COVE..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
COVE descargado
|
|
</label>
|
|
<select
|
|
value={covesFilters.cove_descargado}
|
|
onChange={(e) => setCovesFilters(prev => ({ ...prev, cove_descargado: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="true">Descargado</option>
|
|
<option value="false">No descargado</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Acuse descargado
|
|
</label>
|
|
<select
|
|
value={covesFilters.acuse_cove_descargado}
|
|
onChange={(e) => setCovesFilters(prev => ({ ...prev, acuse_cove_descargado: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="true">Descargado</option>
|
|
<option value="false">No descargado</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fecha desde
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={covesFilters.created_at__gte}
|
|
onChange={(e) => setCovesFilters(prev => ({ ...prev, created_at__gte: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{covesLoading ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span className="ml-2 text-gray-600">Cargando COVEs...</span>
|
|
</div>
|
|
) : covesError ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-red-600 mb-2">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">Error al cargar COVEs</h3>
|
|
<p className="text-gray-600">{covesError}</p>
|
|
</div>
|
|
) : coves.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay COVEs</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
No se encontraron COVEs para este pedimento.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Tabla de COVEs */}
|
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-300">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Número COVE
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Fecha de Creación
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estado COVE
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estado Acuse
|
|
</th>
|
|
<th scope="col" className="relative px-6 py-3">
|
|
<span className="sr-only">Acciones</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{coves.map((cove, index) => (
|
|
<tr key={`${cove.id}-${index}`} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{cove.numero_cove}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(cove.created_at)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
cove.cove_descargado
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{cove.cove_descargado ? 'Descargado' : 'Pendiente'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
cove.acuse_cove_descargado
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{cove.acuse_cove_descargado ? 'Descargado' : 'Pendiente'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div className="flex items-center justify-end space-x-2">
|
|
{/* Botón COVE */}
|
|
<button
|
|
onClick={() => handleCoveProcess(cove)}
|
|
disabled={cove.cove_descargado || processingCove === cove.id}
|
|
className={`p-1 rounded transition-colors ${
|
|
cove.cove_descargado
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: processingCove === cove.id
|
|
? 'text-blue-400 cursor-not-allowed'
|
|
: 'text-blue-600 hover:text-blue-900'
|
|
}`}
|
|
title={cove.cove_descargado ? 'COVE ya descargado' : processingCove === cove.id ? 'Procesando COVE...' : 'Procesar COVE'}
|
|
>
|
|
{processingCove === cove.id ? (
|
|
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h8m-10-4a9 9 0 1118 0 9 9 0 01-18 0z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Botón Acuse de COVE */}
|
|
<button
|
|
onClick={() => handleAcuseCoveProcess(cove)}
|
|
disabled={cove.acuse_cove_descargado || processingAcuseCove === cove.id}
|
|
className={`p-1 rounded transition-colors ${
|
|
cove.acuse_cove_descargado
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: processingAcuseCove === cove.id
|
|
? 'text-green-400 cursor-not-allowed'
|
|
: 'text-green-600 hover:text-green-900'
|
|
}`}
|
|
title={cove.acuse_cove_descargado ? 'Acuse de COVE ya descargado' : processingAcuseCove === cove.id ? 'Procesando Acuse de COVE...' : 'Procesar Acuse de COVE'}
|
|
>
|
|
{processingAcuseCove === cove.id ? (
|
|
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paginación para COVEs */}
|
|
{covesCount > 0 && (
|
|
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<span className="text-sm text-gray-700">
|
|
<span className="hidden sm:inline">Mostrando </span>
|
|
<span className="font-medium">{((covesPage - 1) * covesPageSize) + 1}</span>-
|
|
<span className="font-medium">{Math.min(covesPage * covesPageSize, covesCount)}</span>
|
|
<span className="hidden sm:inline"> de </span>
|
|
<span className="sm:hidden">/</span>
|
|
<span className="font-medium">{covesCount}</span>
|
|
<span className="hidden sm:inline"> COVEs</span>
|
|
</span>
|
|
<select
|
|
value={covesPageSize}
|
|
onChange={(e) => {
|
|
setCovesPageSize(Number(e.target.value));
|
|
setCovesPage(1);
|
|
}}
|
|
className="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto"
|
|
>
|
|
<option value={10}>10 por página</option>
|
|
<option value={25}>25 por página</option>
|
|
<option value={50}>50 por página</option>
|
|
<option value={100}>100 por página</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center sm:justify-end space-x-1 sm:space-x-2">
|
|
<button
|
|
onClick={() => setCovesPage(Math.max(1, covesPage - 1))}
|
|
disabled={covesPage === 1}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Anterior</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
|
Página {covesPage} de {Math.ceil(covesCount / covesPageSize)}
|
|
</span>
|
|
|
|
<button
|
|
onClick={() => setCovesPage(Math.min(Math.ceil(covesCount / covesPageSize), covesPage + 1))}
|
|
disabled={covesPage >= Math.ceil(covesCount / covesPageSize)}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Siguiente</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'edocs' && (
|
|
<div className="p-6">
|
|
{/* Header de la sección */}
|
|
<div className="mb-6 space-y-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
<div className="flex items-center space-x-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">
|
|
EDocs del Pedimento
|
|
</h3>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
{edocsCount} EDocs
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filtros */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg border">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Buscar por número EDocs
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={edocsFilters.numero_edocument}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, numero_edocument: e.target.value }))}
|
|
placeholder="Número EDocs..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Clave
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={edocsFilters.clave}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, clave: e.target.value }))}
|
|
placeholder="Clave..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Descripción
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={edocsFilters.descripcion}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, descripcion: e.target.value }))}
|
|
placeholder="Descripción..."
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
EDocs descargado
|
|
</label>
|
|
<select
|
|
value={edocsFilters.edocument_descargado}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, edocument_descargado: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="true">Descargado</option>
|
|
<option value="false">No descargado</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Acuse descargado
|
|
</label>
|
|
<select
|
|
value={edocsFilters.acuse_descargado}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, acuse_descargado: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="true">Descargado</option>
|
|
<option value="false">No descargado</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fecha desde
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={edocsFilters.created_at__gte}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, created_at__gte: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Fecha hasta
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={edocsFilters.created_at__lte}
|
|
onChange={(e) => setEdocsFilters(prev => ({ ...prev, created_at__lte: e.target.value }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{edocsLoading ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
|
<span className="ml-2 text-gray-600">Cargando EDocs...</span>
|
|
</div>
|
|
) : edocsError ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-red-600 mb-2">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">Error al cargar EDocs</h3>
|
|
<p className="text-gray-600">{edocsError}</p>
|
|
</div>
|
|
) : edocs.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay EDocs</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
No se encontraron documentos electrónicos para este pedimento.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Tabla de EDocs */}
|
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
|
|
<table className="min-w-full divide-y divide-gray-300">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Número EDocs
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Clave
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Descripción
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Fecha de Creación
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estado EDocs
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estado Acuse
|
|
</th>
|
|
<th scope="col" className="relative px-6 py-3">
|
|
<span className="sr-only">Acciones</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{edocs.map((edoc, index) => (
|
|
<tr key={`${edoc.id}-${index}`} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-lg bg-green-100 flex items-center justify-center">
|
|
<svg className="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{edoc.numero_edocument}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
{edoc.clave || 'N/A'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 max-w-xs truncate" title={edoc.descripcion}>
|
|
{edoc.descripcion || 'Sin descripción'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(edoc.created_at)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
edoc.edocument_descargado
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{edoc.edocument_descargado ? 'Descargado' : 'Pendiente'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
edoc.acuse_descargado
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{edoc.acuse_descargado ? 'Descargado' : 'Pendiente'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div className="flex items-center justify-end space-x-2">
|
|
{/* Botón EDoc */}
|
|
<button
|
|
onClick={() => handleEdocProcess(edoc)}
|
|
disabled={edoc.edocument_descargado || processingEdoc === edoc.id}
|
|
className={`p-1 rounded transition-colors ${
|
|
edoc.edocument_descargado
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: processingEdoc === edoc.id
|
|
? 'text-blue-400 cursor-not-allowed'
|
|
: 'text-blue-600 hover:text-blue-900'
|
|
}`}
|
|
title={edoc.edocument_descargado ? 'EDoc ya descargado' : processingEdoc === edoc.id ? 'Procesando EDoc...' : 'Procesar EDoc'}
|
|
>
|
|
{processingEdoc === edoc.id ? (
|
|
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Botón Acuse de EDoc */}
|
|
<button
|
|
onClick={() => handleAcuseEdocProcess(edoc)}
|
|
disabled={edoc.acuse_descargado || processingAcuseEdoc === edoc.id}
|
|
className={`p-1 rounded transition-colors ${
|
|
edoc.acuse_descargado
|
|
? 'text-gray-400 cursor-not-allowed'
|
|
: processingAcuseEdoc === edoc.id
|
|
? 'text-green-400 cursor-not-allowed'
|
|
: 'text-green-600 hover:text-green-900'
|
|
}`}
|
|
title={edoc.acuse_descargado ? 'Acuse de EDoc ya descargado' : processingAcuseEdoc === edoc.id ? 'Procesando Acuse de EDoc...' : 'Procesar Acuse de EDoc'}
|
|
>
|
|
{processingAcuseEdoc === edoc.id ? (
|
|
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paginación para EDocs */}
|
|
{edocsCount > 0 && (
|
|
<div className="mt-6 flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-sm text-gray-700">
|
|
Mostrando <span className="font-medium">{((edocsPage - 1) * edocsPageSize) + 1}</span> a{' '}
|
|
<span className="font-medium">{Math.min(edocsPage * edocsPageSize, edocsCount)}</span> de{' '}
|
|
<span className="font-medium">{edocsCount}</span> EDocs
|
|
</span>
|
|
<select
|
|
value={edocsPageSize}
|
|
onChange={(e) => {
|
|
setEdocsPageSize(Number(e.target.value));
|
|
setEdocsPage(1);
|
|
}}
|
|
className="ml-2 text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value={10}>10 por página</option>
|
|
<option value={25}>25 por página</option>
|
|
<option value={50}>50 por página</option>
|
|
<option value={100}>100 por página</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => setEdocsPage(Math.max(1, edocsPage - 1))}
|
|
disabled={edocsPage === 1}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Anterior</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
|
Página {edocsPage} de {Math.ceil(edocsCount / edocsPageSize)}
|
|
</span>
|
|
|
|
<button
|
|
onClick={() => setEdocsPage(Math.min(Math.ceil(edocsCount / edocsPageSize), edocsPage + 1))}
|
|
disabled={edocsPage >= Math.ceil(edocsCount / edocsPageSize)}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Siguiente</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'procesos' && (
|
|
<div className="p-6">
|
|
{/* Header de la sección */}
|
|
<div className="mb-4 sm:mb-6">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
|
Procesos del Pedimento
|
|
</h3>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 w-fit">
|
|
{procesosCount} Procesos
|
|
</span>
|
|
</div>
|
|
|
|
{/* Botones de creación de servicios */}
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-3">
|
|
{/* Botón prioritario: Obtener pedimento completo */}
|
|
{!existeServicio(3) && (
|
|
<button
|
|
onClick={() => handleCrearServicio(3, 'Procesamiento SAT')}
|
|
disabled={creatingService === 3}
|
|
className={`inline-flex items-center px-3 sm:px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white transition-colors duration-200 w-full sm:w-auto justify-center ${
|
|
creatingService === 3
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
|
}`}
|
|
>
|
|
{creatingService === 3 ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span className="hidden sm:inline">Creando...</span>
|
|
<span className="sm:hidden">Creando...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Obtener Pedimento Completo</span>
|
|
<span className="sm:hidden">Obtener Pedimento</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
|
|
{/* Otros servicios disponibles */}
|
|
{existeServicio(3) && (
|
|
<div className="flex items-center space-x-2">
|
|
{/* Generación COVEs */}
|
|
{!existeServicio(4) && (
|
|
<button
|
|
onClick={() => handleCrearServicio(4, 'Generación COVEs')}
|
|
disabled={creatingService === 4}
|
|
className={`inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors duration-200 ${
|
|
creatingService === 4 ? 'opacity-50 cursor-not-allowed' : ''
|
|
}`}
|
|
>
|
|
{creatingService === 4 ? (
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
) : (
|
|
<svg className="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
)}
|
|
COVEs
|
|
</button>
|
|
)}
|
|
|
|
{/* Generación EDocs */}
|
|
{!existeServicio(5) && (
|
|
<button
|
|
onClick={() => handleCrearServicio(5, 'Generación EDocs')}
|
|
disabled={creatingService === 5}
|
|
className={`inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors duration-200 ${
|
|
creatingService === 5 ? 'opacity-50 cursor-not-allowed' : ''
|
|
}`}
|
|
>
|
|
{creatingService === 5 ? (
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
) : (
|
|
<svg className="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
)}
|
|
EDocs
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{procesosLoading ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
|
<span className="ml-2 text-gray-600">Cargando procesos...</span>
|
|
</div>
|
|
) : procesosError ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-red-600 mb-2">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">Error al cargar procesos</h3>
|
|
<p className="text-gray-600">{procesosError}</p>
|
|
</div>
|
|
) : procesos.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No hay procesos</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
No se encontraron procesos para este pedimento.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Tabla de Procesos */}
|
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-300">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
ID Proceso
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Servicio
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Estado
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Organización
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Fecha Creación
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Última Actualización
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Acciones
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{procesos.map((proceso, index) => (
|
|
<tr key={`${proceso.id}-${index}`} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-lg bg-purple-100 flex items-center justify-center">
|
|
<svg className="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
#{proceso.id}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getServicioColor(proceso.servicio)}`}>
|
|
{getServicioLabel(proceso.servicio)}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getEstadoColor(proceso.estado)}`}>
|
|
{getEstadoLabel(proceso.estado)}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{proceso.organizacion_name || 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(proceso.created_at)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(proceso.updated_at)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<div className="flex items-center space-x-2">
|
|
{/* Botón Play (Ejecutar Servicio) */}
|
|
<button
|
|
onClick={() => handleEjecutarServicio(proceso)}
|
|
disabled={
|
|
executingId === proceso.id ||
|
|
proceso.estado === 2 || // En Proceso
|
|
proceso.estado === 3 || // Completado
|
|
proceso.estado === 5 // Cancelado
|
|
}
|
|
className={`inline-flex items-center p-2 border border-transparent rounded-md transition-colors duration-200 ${
|
|
executingId === proceso.id ||
|
|
proceso.estado === 2 ||
|
|
proceso.estado === 3 ||
|
|
proceso.estado === 5
|
|
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
|
: 'bg-green-100 text-green-700 hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500'
|
|
}`}
|
|
title={
|
|
executingId === proceso.id ? 'Ejecutando...' :
|
|
proceso.estado === 2 ? 'No disponible - En proceso' :
|
|
proceso.estado === 3 ? 'No disponible - Completado' :
|
|
proceso.estado === 5 ? 'No disponible - Cancelado' :
|
|
'Ejecutar servicio'
|
|
}
|
|
>
|
|
{executingId === proceso.id ? (
|
|
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
) : (
|
|
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z"/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Botón Refresh (Pasar a Espera) */}
|
|
<button
|
|
onClick={() => handlePasarAEspera(proceso)}
|
|
disabled={
|
|
changingStateId === proceso.id ||
|
|
proceso.estado === 1 || // Pendiente
|
|
proceso.estado === 2 || // En Proceso
|
|
proceso.estado === 3 || // Completado
|
|
proceso.estado === 5 // Cancelado
|
|
}
|
|
className={`inline-flex items-center p-2 border border-transparent rounded-md transition-colors duration-200 ${
|
|
changingStateId === proceso.id ||
|
|
proceso.estado === 1 ||
|
|
proceso.estado === 2 ||
|
|
proceso.estado === 3 ||
|
|
proceso.estado === 5
|
|
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
|
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500'
|
|
}`}
|
|
title={
|
|
changingStateId === proceso.id ? 'Cambiando estado...' :
|
|
proceso.estado === 1 ? 'No disponible - Use Play para ejecutar' :
|
|
proceso.estado === 2 ? 'No disponible - En proceso' :
|
|
proceso.estado === 3 ? 'No disponible - Completado' :
|
|
proceso.estado === 5 ? 'No disponible - Cancelado' :
|
|
'Pasar a espera'
|
|
}
|
|
>
|
|
{changingStateId === proceso.id ? (
|
|
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
) : (
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paginación para Procesos */}
|
|
{procesosCount > 0 && (
|
|
<div className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<span className="text-sm text-gray-700">
|
|
<span className="hidden sm:inline">Mostrando </span>
|
|
<span className="font-medium">{((procesosPage - 1) * procesosPageSize) + 1}</span>-
|
|
<span className="font-medium">{Math.min(procesosPage * procesosPageSize, procesosCount)}</span>
|
|
<span className="hidden sm:inline"> de </span>
|
|
<span className="sm:hidden">/</span>
|
|
<span className="font-medium">{procesosCount}</span>
|
|
<span className="hidden sm:inline"> procesos</span>
|
|
</span>
|
|
<select
|
|
value={procesosPageSize}
|
|
onChange={(e) => {
|
|
setProcesosPageSize(Number(e.target.value));
|
|
setProcesosPage(1);
|
|
}}
|
|
className="text-sm border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto"
|
|
>
|
|
<option value={10}>10 por página</option>
|
|
<option value={25}>25 por página</option>
|
|
<option value={50}>50 por página</option>
|
|
<option value={100}>100 por página</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center sm:justify-end space-x-1 sm:space-x-2">
|
|
<button
|
|
onClick={() => setProcesosPage(Math.max(1, procesosPage - 1))}
|
|
disabled={procesosPage === 1}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Anterior</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
|
Página {procesosPage} de {Math.ceil(procesosCount / procesosPageSize)}
|
|
</span>
|
|
|
|
<button
|
|
onClick={() => setProcesosPage(Math.min(Math.ceil(procesosCount / procesosPageSize), procesosPage + 1))}
|
|
disabled={procesosPage >= Math.ceil(procesosCount / procesosPageSize)}
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="sr-only">Siguiente</span>
|
|
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'auditor' && (
|
|
<div className="p-6">
|
|
{/* Header de la sección */}
|
|
<div className="mb-4 sm:mb-6">
|
|
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
|
Auditoría del Pedimento
|
|
</h3>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 w-fit">
|
|
Análisis Completo
|
|
</span>
|
|
</div>
|
|
|
|
{/* Botones de Auditoría */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 sm:gap-3 w-full lg:w-auto">
|
|
<button
|
|
onClick={() => handleAuditarPedimentoCompleto()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditar Pedimento Completo</span>
|
|
<span className="sm:hidden">Ped. Completo</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleAuditarEDocs()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditar EDocs</span>
|
|
<span className="sm:hidden">EDocs</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleAuditarPartidas()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V17m0-10a2 2 0 012-2h2a2 2 0 002-2M13 7h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditar Partidas</span>
|
|
<span className="sm:hidden">Partidas</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleAuditarAcuses()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditar Acuses</span>
|
|
<span className="sm:hidden">Acuses</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleAuditarCoves()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditar COVEs</span>
|
|
<span className="sm:hidden">COVEs</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleAuditarAcuseCoves()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-gray-600 to-gray-700 hover:from-gray-700 hover:to-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200 shadow-md hover:shadow-lg w-full"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Auditar Acuse COVEs</span>
|
|
<span className="sm:hidden">Ac. COVEs</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleCompararRemesasCoves()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all duration-200 shadow-md hover:shadow-lg w-full sm:col-span-2 lg:col-span-1"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Comparar Remesas vs COVEs</span>
|
|
<span className="sm:hidden">Comparar</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleVerificarServiciosCreados()}
|
|
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200 shadow-md hover:shadow-lg w-full sm:col-span-2 lg:col-span-1"
|
|
>
|
|
<svg className="w-4 h-4 mr-1 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Verificar Servicios Creados</span>
|
|
<span className="sm:hidden">Verificar</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contenido del Auditor */}
|
|
<div className="space-y-6">
|
|
{/* Resumen de Estado */}
|
|
<div className="bg-gradient-to-r from-green-50 to-blue-50 rounded-xl p-6 border border-green-200">
|
|
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<svg className="w-5 h-5 mr-2 text-green-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>
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
<div className="bg-white rounded-lg p-4 shadow-sm border">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">Documentos</p>
|
|
<p className="text-2xl font-bold text-blue-600">{docsCount || 0}</p>
|
|
</div>
|
|
<svg className="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg p-4 shadow-sm border">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">COVEs</p>
|
|
<p className="text-2xl font-bold text-purple-600">{covesCount || 0}</p>
|
|
</div>
|
|
<svg className="w-8 h-8 text-purple-500" 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="bg-white rounded-lg p-4 shadow-sm border">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">EDocs</p>
|
|
<p className="text-2xl font-bold text-teal-600">{edocsCount || 0}</p>
|
|
</div>
|
|
<svg className="w-8 h-8 text-teal-500" 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="bg-white rounded-lg p-4 shadow-sm border">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">Procesos</p>
|
|
<p className="text-2xl font-bold text-orange-600">{procesosCount || 0}</p>
|
|
</div>
|
|
<svg className="w-8 h-8 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Análisis de Integridad */}
|
|
<div className="bg-white rounded-xl p-6 shadow-lg border">
|
|
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
Análisis de Integridad
|
|
</h4>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span className="font-medium text-green-800">Documentos Requeridos</span>
|
|
</div>
|
|
<span className="text-green-600 font-semibold">Completo</span>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span className="font-medium text-blue-800">Validación de Estructura</span>
|
|
</div>
|
|
<span className="text-blue-600 font-semibold">Verificado</span>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 text-purple-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
<span className="font-medium text-purple-800">Seguridad y Firmas</span>
|
|
</div>
|
|
<span className="text-purple-600 font-semibold">Auditado</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timeline de Actividades */}
|
|
<div className="bg-white rounded-xl p-6 shadow-lg border">
|
|
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<svg className="w-5 h-5 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Timeline de Actividades Recientes
|
|
</h4>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-start">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
|
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4 flex-1">
|
|
<p className="text-sm font-medium text-gray-900">Pedimento procesado correctamente</p>
|
|
<p className="text-sm text-gray-500">Todos los documentos han sido validados y procesados</p>
|
|
<p className="text-xs text-gray-400 mt-1">{new Date().toLocaleDateString('es-ES')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4 flex-1">
|
|
<p className="text-sm font-medium text-gray-900">Documentos cargados</p>
|
|
<p className="text-sm text-gray-500">Se han cargado {docsCount || 0} documentos al expediente</p>
|
|
<p className="text-xs text-gray-400 mt-1">{pedimento?.created_at ? new Date(pedimento.created_at).toLocaleDateString('es-ES') : 'Fecha no disponible'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-start">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
|
<svg className="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4 flex-1">
|
|
<p className="text-sm font-medium text-gray-900">Expediente creado</p>
|
|
<p className="text-sm text-gray-500">Expediente #{pedimento?.pedimento_app || 'N/A'} iniciado</p>
|
|
<p className="text-xs text-gray-400 mt-1">{pedimento?.created_at ? new Date(pedimento.created_at).toLocaleDateString('es-ES') : 'Fecha no disponible'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal de vista previa mejorado */}
|
|
{previewOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
|
<div className="bg-white/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-blue-200/50 overflow-hidden relative flex flex-col w-full max-w-6xl h-full max-h-[95vh]">
|
|
{/* Header del modal con gradiente e información del archivo */}
|
|
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-blue-600 to-blue-800 text-white">
|
|
<div className="flex items-center gap-4 flex-1 min-w-0">
|
|
<div className="bg-white/20 rounded-xl p-2 flex-shrink-0">
|
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-xl font-bold truncate">Vista previa de documento</h3>
|
|
{previewDoc && (
|
|
<div className="flex items-center gap-3 mt-1 text-sm text-blue-100">
|
|
<span className="truncate">{previewDoc.archivo_original || 'Sin nombre'}</span>
|
|
{previewDoc.size && (
|
|
<span className="bg-white/20 px-2 py-0.5 rounded-lg text-xs font-medium">
|
|
{formatFileSize(previewDoc.size)}
|
|
</span>
|
|
)}
|
|
{previewDoc.extension && (
|
|
<span className="bg-white/20 px-2 py-0.5 rounded-lg text-xs font-medium uppercase">
|
|
{previewDoc.extension}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Controles del header */}
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{/* Botón de descarga siempre visible */}
|
|
{previewDoc && (
|
|
<button
|
|
onClick={() => downloadDocument(previewDoc)}
|
|
className="p-2 bg-white/20 hover:bg-white/30 rounded-xl transition-colors duration-200 group"
|
|
title="Descargar documento"
|
|
>
|
|
<svg className="h-5 w-5 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
|
|
{/* Controles de zoom para imágenes */}
|
|
{previewType === 'img' && (
|
|
<div className="flex items-center gap-1 bg-white/20 rounded-xl p-1">
|
|
<button
|
|
onClick={() => setImageZoom(Math.max(0.2, imageZoom - 0.2))}
|
|
className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-xs"
|
|
title="Alejar"
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 12H4" />
|
|
</svg>
|
|
</button>
|
|
<span className="text-xs font-medium px-2">{Math.round(imageZoom * 100)}%</span>
|
|
<button
|
|
onClick={() => setImageZoom(Math.min(3, imageZoom + 0.2))}
|
|
className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-xs"
|
|
title="Acercar"
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => setImageZoom(1)}
|
|
className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-xs"
|
|
title="Tamaño original"
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 8V4m0 0h4M4 4l5 5m11-5v4m0-4h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Botón cerrar */}
|
|
<button
|
|
onClick={handleClosePreview}
|
|
className="p-2 bg-white/20 hover:bg-red-500/80 rounded-xl transition-colors duration-200"
|
|
title="Cerrar vista previa"
|
|
>
|
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contenido del modal */}
|
|
<div className="flex-1 flex flex-col min-h-0 bg-gray-50">
|
|
{previewLoading ? (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="inline-flex items-center space-x-3 text-blue-600">
|
|
<svg className="animate-spin h-10 w-10" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span className="text-lg font-medium">Cargando documento...</span>
|
|
</div>
|
|
<p className="text-gray-500 mt-2">Preparando vista previa</p>
|
|
</div>
|
|
</div>
|
|
) : previewError ? (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="text-center text-red-600 max-w-md">
|
|
<div className="bg-red-50 rounded-full w-20 h-20 mx-auto mb-4 flex items-center justify-center">
|
|
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h4 className="text-lg font-semibold mb-2">Error al cargar documento</h4>
|
|
<p className="text-sm text-gray-600">{previewError}</p>
|
|
<button
|
|
onClick={() => setPreviewError(null)}
|
|
className="mt-4 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg transition-colors"
|
|
>
|
|
Reintentar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : previewType === 'pdf' ? (
|
|
<div className="flex-1 bg-white">
|
|
<iframe
|
|
src={previewUrl}
|
|
title="PDF Preview"
|
|
className="w-full h-full border-0"
|
|
style={{ minHeight: '500px' }}
|
|
/>
|
|
</div>
|
|
) : previewType === 'img' ? (
|
|
<div className="flex-1 flex items-center justify-center p-4 overflow-auto bg-gray-100">
|
|
<div className="relative">
|
|
<img
|
|
src={previewUrl}
|
|
alt="Vista previa"
|
|
className="max-w-none shadow-lg rounded-lg border-2 border-white"
|
|
style={{
|
|
transform: `scale(${imageZoom})`,
|
|
transformOrigin: 'center',
|
|
transition: 'transform 0.2s ease'
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : previewType === 'xml' ? (
|
|
<div className="flex-1 flex flex-col bg-white">
|
|
<div className="bg-gray-100 px-6 py-3 border-b border-gray-200 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="bg-green-100 p-1.5 rounded-lg">
|
|
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<span className="font-medium text-gray-700">Documento XML</span>
|
|
</div>
|
|
<button
|
|
className="inline-flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800 px-4 py-2 rounded-lg border border-blue-300 bg-blue-50 hover:bg-blue-100 transition-colors"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(previewXml);
|
|
// Mostrar notificación de copiado
|
|
}}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
Copiar XML
|
|
</button>
|
|
</div>
|
|
<pre
|
|
className="flex-1 p-6 overflow-auto text-sm bg-white border-l-4 border-green-400"
|
|
style={{
|
|
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
|
lineHeight: '1.6'
|
|
}}
|
|
dangerouslySetInnerHTML={{ __html: previewXmlHtml }}
|
|
/>
|
|
</div>
|
|
) : previewType === 'txt' ? (
|
|
<div className="flex-1 bg-white p-6 overflow-auto">
|
|
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<div className="bg-blue-100 p-1.5 rounded-lg">
|
|
<svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<span className="font-medium text-gray-700">Documento de texto</span>
|
|
</div>
|
|
<pre className="text-sm text-gray-800 whitespace-pre-wrap font-mono leading-relaxed">
|
|
{previewContent || 'Contenido no disponible'}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
) : previewUrl ? (
|
|
<div className="flex-1 flex items-center justify-center p-8 bg-gray-50">
|
|
<div className="text-center">
|
|
<div className="bg-blue-50 rounded-full w-20 h-20 mx-auto mb-6 flex items-center justify-center">
|
|
<svg className="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<h4 className="text-lg font-semibold text-gray-900 mb-2">Vista previa no disponible</h4>
|
|
<p className="text-gray-600 mb-6">Este tipo de archivo no se puede mostrar en el navegador</p>
|
|
<a
|
|
href={previewUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl font-medium transition-colors shadow-lg hover:shadow-xl"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>Descargar archivo</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 flex items-center justify-center p-8">
|
|
<div className="text-center text-gray-500">
|
|
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<p className="text-lg">No se pudo cargar el documento</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |