feature/nueva funcionalidad al hacer login mediante HUB y nuevos reportes en segundo plano
This commit is contained in:
@@ -11,10 +11,14 @@ export const fetchPedimentoCoves = async (pedimentoId, page = 1, pageSize = 10,
|
||||
});
|
||||
|
||||
if (filters.numero_cove) params.append('numero_cove__icontains', filters.numero_cove);
|
||||
// Estado de 3 valores (pendiente | descargado | error) — T2026-05-027
|
||||
if (filters.cove_estado) params.append('cove_estado', filters.cove_estado);
|
||||
if (filters.acuse_cove_estado) params.append('acuse_cove_estado', filters.acuse_cove_estado);
|
||||
// Filtros booleanos legados (compatibilidad)
|
||||
if (filters.cove_descargado !== undefined && filters.cove_descargado !== '') params.append('cove_descargado', filters.cove_descargado);
|
||||
if (filters.acuse_cove_descargado !== undefined && filters.acuse_cove_descargado !== '') params.append('acuse_cove_descargado', filters.acuse_cove_descargado);
|
||||
if (filters.date_from) params.append('created_at__gte', filters.date_from);
|
||||
if (filters.date_to) params.append('created_at__lte', filters.date_to);
|
||||
if (filters.date_from || filters.created_at__gte) params.append('created_at__gte', filters.date_from || filters.created_at__gte);
|
||||
if (filters.date_to || filters.created_at__lte) params.append('created_at__lte', filters.date_to || filters.created_at__lte);
|
||||
|
||||
const response = await fetchWithAuth(`${API_BASE_URL}/customs/coves/?${params}`);
|
||||
if (!response.ok) throw new Error(await extractApiError(response));
|
||||
|
||||
@@ -13,10 +13,14 @@ export const fetchPedimentoEdocuments = async (pedimentoId, page = 1, pageSize =
|
||||
if (filters.numero_edocument) params.append('numero_edocument__icontains', filters.numero_edocument);
|
||||
if (filters.clave) params.append('clave__icontains', filters.clave);
|
||||
if (filters.descripcion) params.append('descripcion__icontains', filters.descripcion);
|
||||
// Estado de 3 valores (pendiente | descargado | error) — T2026-05-027
|
||||
if (filters.edocument_estado) params.append('edocument_estado', filters.edocument_estado);
|
||||
if (filters.acuse_estado) params.append('acuse_estado', filters.acuse_estado);
|
||||
// Filtros booleanos legados (compatibilidad)
|
||||
if (filters.edocument_descargado !== undefined && filters.edocument_descargado !== '') params.append('edocument_descargado', filters.edocument_descargado);
|
||||
if (filters.acuse_descargado !== undefined && filters.acuse_descargado !== '') params.append('acuse_descargado', filters.acuse_descargado);
|
||||
if (filters.date_from) params.append('created_at__gte', filters.date_from);
|
||||
if (filters.date_to) params.append('created_at__lte', filters.date_to);
|
||||
if (filters.date_from || filters.created_at__gte) params.append('created_at__gte', filters.date_from || filters.created_at__gte);
|
||||
if (filters.date_to || filters.created_at__lte) params.append('created_at__lte', filters.date_to || filters.created_at__lte);
|
||||
|
||||
const response = await fetchWithAuth(`${API_BASE_URL}/customs/edocuments/?${params}`);
|
||||
if (!response.ok) throw new Error(await extractApiError(response));
|
||||
|
||||
@@ -48,7 +48,7 @@ function AuditResultPanel({ modal, closing, iniciandoProcesamiento, onClose, onI
|
||||
const sinNadaQueHacer = (tipo === 'rm' && data?.tiene_remesas === false);
|
||||
const esAuditoriaIntegridad = ['int_pt', 'int_edoc', 'int_cove', 'int_rm'].includes(tipo);
|
||||
const mostrarBotonProcesar = !esCompletado && !sinNadaQueHacer && !esAuditoriaIntegridad;
|
||||
const mostrarBotonCorregir = esAuditoriaIntegridad && !esCompletado && data?.estado !== 'sin_xml' && data?.estado !== 'sin_datos_xml' && data?.estado !== 'sin_datos';
|
||||
const mostrarBotonCorregir = esAuditoriaIntegridad && !esCompletado && !['sin_xml', 'sin_datos_xml', 'sin_datos', 'sin_xml_pc', 'descarga_solicitada', 'error'].includes(data?.estado);
|
||||
const tituloTipo = TIPO_LABELS[tipo] || tipo;
|
||||
const hayErroresPC = tipo === 'pc' && data?.hay_errores;
|
||||
|
||||
@@ -2047,19 +2047,18 @@ const handleAuditarEDocumentPedimento = async (pedimentoId) => {
|
||||
const raw = await response.json();
|
||||
const completado = raw.estado === 'completado';
|
||||
const sinRemesas = raw.estado === 'sin_remesas';
|
||||
const soloMensaje = ['sin_remesas', 'sin_xml', 'sin_xml_pc', 'descarga_solicitada', 'error'].includes(raw.estado);
|
||||
const data = {
|
||||
estado: raw.estado,
|
||||
auditoria_completa: completado || sinRemesas,
|
||||
mensaje: completado
|
||||
? `Remesa íntegra: ${raw.coves_db} COVEs de remesa en DB`
|
||||
: sinRemesas
|
||||
? raw.mensaje
|
||||
: raw.estado === 'sin_xml'
|
||||
: soloMensaje
|
||||
? raw.mensaje ?? 'No hay XML de remesa descargado'
|
||||
: `Faltan ${raw.faltantes?.length ?? 0} COVE(s) de remesa en DB`,
|
||||
resumen: completado
|
||||
resumen: (completado || raw.estado === 'incompleto')
|
||||
? { 'COVEs en remesa': raw.total_en_remesa, 'COVEs en DB': raw.coves_db }
|
||||
: (!completado && !sinRemesas && raw.estado !== 'sin_xml') ? { 'COVEs en remesa': raw.total_en_remesa, 'COVEs en DB': raw.coves_db } : null,
|
||||
: null,
|
||||
faltantes: raw.faltantes ?? [],
|
||||
total_en_remesa: raw.total_en_remesa,
|
||||
coves_db: raw.coves_db,
|
||||
|
||||
@@ -2,8 +2,6 @@ import React, { useState } from 'react';
|
||||
import { login, getMicrosoftLoginUrl } from '../api/auth';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const HUB_URL = import.meta.env.VITE_HUB_URL || 'http://localhost:3001';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -33,15 +31,11 @@ export default function Login() {
|
||||
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
if (data.first_login) {
|
||||
// Primera vez: acaba de ser provisionado en Hub.
|
||||
// Redirigir al Hub para que establezca su sesión KC y conozca el workspace.
|
||||
const returnTo = encodeURIComponent('/app-launcher');
|
||||
window.location.href = `${HUB_URL}/login?return_to=${returnTo}`;
|
||||
} else {
|
||||
// Ya estaba migrado: ir directo al dashboard de EFC.
|
||||
window.location.href = '/admin';
|
||||
}
|
||||
// Login único: la sesión local de EFC ya quedó establecida con los tokens
|
||||
// recibidos. En el primer ingreso el backend dispara la provisión/migración
|
||||
// en Hub en segundo plano — no forzamos al usuario a pasar por el Hub.
|
||||
// (El logout sí lo redirige al Hub para cerrar la sesión KC.)
|
||||
window.location.href = '/admin';
|
||||
|
||||
} catch (err) {
|
||||
setError(err.message || 'Usuario o contraseña incorrectos');
|
||||
|
||||
@@ -65,6 +65,32 @@ const getEstadoColor = (estado) => {
|
||||
return colores[estado] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
// Badge de estado de descarga VU de 3 valores (T2026-05-027). Acepta el estado
|
||||
// nuevo ('pendiente' | 'descargado' | 'error') con fallback al booleano legado
|
||||
// mientras la migración no esté aplicada en todos los entornos.
|
||||
const renderEstadoDescargaBadge = (estado, descargadoLegacy, intentos, ultimoError) => {
|
||||
const value = estado || (descargadoLegacy ? 'descargado' : 'pendiente');
|
||||
const estilos = {
|
||||
descargado: 'bg-green-100 text-green-800',
|
||||
pendiente: 'bg-yellow-100 text-yellow-800',
|
||||
error: 'bg-red-100 text-red-800',
|
||||
};
|
||||
const etiquetas = { descargado: 'Descargado', pendiente: 'Pendiente', error: 'Error' };
|
||||
const label = etiquetas[value] || 'Pendiente';
|
||||
const showIntentos = value !== 'descargado' && Number(intentos) > 0;
|
||||
const title = value === 'error'
|
||||
? (ultimoError || 'Error en la descarga; requiere reproceso manual')
|
||||
: (showIntentos ? `Intentos automáticos de descarga: ${intentos}` : undefined);
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${estilos[value] || estilos.pendiente}`}
|
||||
title={title}
|
||||
>
|
||||
{label}{showIntentos ? ` (${intentos})` : ''}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getServicioLabel = (servicio) => {
|
||||
const servicios = {
|
||||
1: 'Estado de Pedimento',
|
||||
@@ -141,6 +167,7 @@ export default function PedimentoDetail() {
|
||||
const [docsPrev, setDocsPrev] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [docsRefreshKey, setDocsRefreshKey] = useState(0);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloadingAll, setDownloadingAll] = useState(false);
|
||||
@@ -200,10 +227,11 @@ export default function PedimentoDetail() {
|
||||
const [covesError, setCovesError] = useState('');
|
||||
const [covesPage, setCovesPage] = useState(1);
|
||||
const [covesPageSize, setCovesPageSize] = useState(10);
|
||||
const [covesRefreshKey, setCovesRefreshKey] = useState(0);
|
||||
const [covesFilters, setCovesFilters] = useState({
|
||||
numero_cove: '',
|
||||
cove_descargado: '',
|
||||
acuse_cove_descargado: '',
|
||||
cove_estado: '',
|
||||
acuse_cove_estado: '',
|
||||
created_at__gte: '',
|
||||
created_at__lte: ''
|
||||
});
|
||||
@@ -215,12 +243,13 @@ export default function PedimentoDetail() {
|
||||
const [edocsError, setEdocsError] = useState('');
|
||||
const [edocsPage, setEdocsPage] = useState(1);
|
||||
const [edocsPageSize, setEdocsPageSize] = useState(10);
|
||||
const [edocsRefreshKey, setEdocsRefreshKey] = useState(0);
|
||||
const [edocsFilters, setEdocsFilters] = useState({
|
||||
numero_edocument: '',
|
||||
clave: '',
|
||||
descripcion: '',
|
||||
edocument_descargado: '',
|
||||
acuse_descargado: '',
|
||||
edocument_estado: '',
|
||||
acuse_estado: '',
|
||||
created_at__gte: '',
|
||||
created_at__lte: ''
|
||||
});
|
||||
@@ -234,6 +263,7 @@ export default function PedimentoDetail() {
|
||||
const [pedimentoError, setPedimentoError] = useState('');
|
||||
const [pedimentoPage, setPedimentoPage] = useState(1);
|
||||
const [pedimentoPageSize, setPedimentoPageSize] = useState(10);
|
||||
const [pedimentoRefreshKey, setPedimentoRefreshKey] = useState(0);
|
||||
const [downloadingAllPedimento, setDownloadingAllPedimento] = useState(false);
|
||||
// Agrega estos estados para selección de documentos de pedimento
|
||||
const [selectedPedimentoDocuments, setSelectedPedimentoDocuments] = useState([]);
|
||||
@@ -331,6 +361,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
const [partidasError, setPartidasError] = useState('');
|
||||
const [partidasPage, setPartidasPage] = useState(1);
|
||||
const [partidasPageSize, setPartidasPageSize] = useState(10);
|
||||
const [partidasRefreshKey, setPartidasRefreshKey] = useState(0);
|
||||
const [partidasFilters, setPartidasFilters] = useState({
|
||||
numero_partida: '',
|
||||
descargado: '',
|
||||
@@ -1053,6 +1084,11 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
formData.append('document_type_id', headerUploadTypeId);
|
||||
}
|
||||
|
||||
// En el apartado Pedimento el backend clasifica el XML (PC/Remesa) y le asigna la nomenclatura
|
||||
if (activeTab === 'pedimento') {
|
||||
formData.append('tab_seccion', 'pedimento');
|
||||
}
|
||||
|
||||
// Agregar archivos al FormData
|
||||
selectedFiles.forEach((file) => {
|
||||
formData.append('files', file);
|
||||
@@ -1068,17 +1104,30 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
showMessage(result.message || `${result.uploaded_count || selectedFiles.length} archivo(s) subido(s) exitosamente`, 'success');
|
||||
|
||||
// Solo reportar éxito por lo realmente subido; los fallos se detallan (status 207)
|
||||
const okCount = (result.created_count || 0) + (result.replaced_count || 0);
|
||||
if (result.failed_files?.length > 0) {
|
||||
const detalle = (result.errors || []).join(' | ');
|
||||
if (okCount > 0) {
|
||||
showMessage(`${okCount} archivo(s) subido(s); ${result.failed_files.length} fallaron: ${detalle}`, 'warning');
|
||||
} else {
|
||||
showMessage(`No fue posible subir los documentos: ${detalle}`, 'error');
|
||||
}
|
||||
} else {
|
||||
showMessage(result.message || `${okCount || selectedFiles.length} archivo(s) subido(s) exitosamente`, 'success');
|
||||
}
|
||||
|
||||
// Limpiar archivos seleccionados y cerrar modal
|
||||
setSelectedFiles([]);
|
||||
setShowUploadModal(false);
|
||||
setHeaderUploadTypeId(null);
|
||||
|
||||
// Forzar recarga de documentos
|
||||
const currentPage = page;
|
||||
setPage(0);
|
||||
setTimeout(() => setPage(currentPage), 100);
|
||||
// Recargar el listado del tab activo y el de Documentos generales
|
||||
if (activeTab === 'pedimento') {
|
||||
setPedimentoRefreshKey(prev => prev + 1);
|
||||
}
|
||||
setDocsRefreshKey(prev => prev + 1);
|
||||
} catch (error) {
|
||||
console.error('Error durante la subida:', error);
|
||||
showMessage(`Error durante la subida: ${error.message}`, 'error');
|
||||
@@ -1182,7 +1231,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
}
|
||||
setDocsLoading(false);
|
||||
});
|
||||
}, [id, page, pageSize, filters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir, showMessage]);
|
||||
}, [id, page, pageSize, docsRefreshKey, filters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir, showMessage]);
|
||||
|
||||
// Resetear página cuando cambien los filtros
|
||||
useEffect(() => {
|
||||
@@ -1244,7 +1293,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
setPedimentoLoading(false);
|
||||
});
|
||||
|
||||
},[id, activeTab, pedimentoPage, pedimentoPageSize, showMessage, pedimentoFilters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir]);
|
||||
},[id, activeTab, pedimentoPage, pedimentoPageSize, pedimentoRefreshKey, showMessage, pedimentoFilters, documentTypeFilter, fileNameFilter, extensionFilter, dateFilter, orderBy, orderDir]);
|
||||
|
||||
|
||||
|
||||
@@ -1270,7 +1319,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
}
|
||||
setCovesLoading(false);
|
||||
});
|
||||
}, [id, activeTab, covesPage, covesPageSize, covesFilters, showMessage]);
|
||||
}, [id, activeTab, covesPage, covesPageSize, covesFilters, covesRefreshKey, showMessage]);
|
||||
|
||||
// Resetear página de COVEs cuando cambien los filtros
|
||||
useEffect(() => {
|
||||
@@ -1299,7 +1348,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
}
|
||||
setEdocsLoading(false);
|
||||
});
|
||||
}, [id, activeTab, edocsPage, edocsPageSize, edocsFilters, showMessage]);
|
||||
}, [id, activeTab, edocsPage, edocsPageSize, edocsFilters, edocsRefreshKey, showMessage]);
|
||||
|
||||
// Resetear página de EDocs cuando cambien los filtros
|
||||
useEffect(() => {
|
||||
@@ -1883,7 +1932,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
}
|
||||
setPartidasLoading(false);
|
||||
});
|
||||
}, [id, activeTab, partidasPage, partidasPageSize, partidasFilters, showMessage, pedimento]);
|
||||
}, [id, activeTab, partidasPage, partidasPageSize, partidasFilters, partidasRefreshKey, showMessage, pedimento]);
|
||||
|
||||
// Resetear página de Partidas cuando cambien los filtros
|
||||
useEffect(() => {
|
||||
@@ -2362,17 +2411,17 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Restablece acuse_descargado=False (crea doc error tipo 26 para Errores VU)
|
||||
// y lanza inmediatamente el reprocesamiento del acuse.
|
||||
// Restablece el acuse a 'pendiente' con contador en 0 (crea doc error tipo 26
|
||||
// para Errores VU) y lanza inmediatamente el reprocesamiento del acuse.
|
||||
const handleResetAcuse = async (edoc) => {
|
||||
try {
|
||||
await resetAcuseEdocument(edoc.id);
|
||||
showMessage(`Restableciendo acuse de ${edoc.numero_edocument}. Iniciando reprocesamiento...`, 'info');
|
||||
await handleAcuseEdocProcess(edoc);
|
||||
fetchEdocs(id, edocsPage, edocsPageSize, edocsFilters);
|
||||
setEdocsRefreshKey(k => k + 1);
|
||||
} catch (error) {
|
||||
showMessage(`Error al restablecer el acuse: ${error.message}`, 'error');
|
||||
fetchEdocs(id, edocsPage, edocsPageSize, edocsFilters);
|
||||
setEdocsRefreshKey(k => k + 1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2462,12 +2511,17 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
'success'
|
||||
);
|
||||
|
||||
// Recargar en una página válida: tras el borrado la página actual puede ya no existir
|
||||
const remainingPartidas = Math.max(0, partidasCount - selectedPartidas.length);
|
||||
const lastPartidasPage = Math.max(1, Math.ceil(remainingPartidas / partidasPageSize));
|
||||
|
||||
setSelectedPartidas([]);
|
||||
|
||||
// Forzar recarga de documentos
|
||||
const currentPage = partidasPage;
|
||||
setPartidasPage(0);
|
||||
setTimeout(() => setPartidasPage(currentPage), 100);
|
||||
if (partidasPage > lastPartidasPage) {
|
||||
setPartidasPage(lastPartidasPage);
|
||||
} else {
|
||||
setPartidasRefreshKey(prev => prev + 1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
|
||||
@@ -2558,12 +2612,17 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
'success'
|
||||
);
|
||||
|
||||
// Recargar en una página válida: tras el borrado la página actual puede ya no existir
|
||||
const remainingCoves = Math.max(0, covesCount - selectedCoves.length);
|
||||
const lastCovesPage = Math.max(1, Math.ceil(remainingCoves / covesPageSize));
|
||||
|
||||
setSelectedCoves([]);
|
||||
|
||||
// Forzar recarga de documentos
|
||||
// const currentPage = covesPage;
|
||||
setCovesPage(0);
|
||||
setTimeout(() => setCovesPage(1), 100);
|
||||
if (covesPage > lastCovesPage) {
|
||||
setCovesPage(lastCovesPage);
|
||||
} else {
|
||||
setCovesRefreshKey(prev => prev + 1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
|
||||
@@ -2723,12 +2782,17 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
'success'
|
||||
);
|
||||
|
||||
// Recargar en una página válida: tras el borrado la página actual puede ya no existir
|
||||
const remainingEdocs = Math.max(0, edocsCount - selectedEdocs.length);
|
||||
const lastEdocsPage = Math.max(1, Math.ceil(remainingEdocs / edocsPageSize));
|
||||
|
||||
setSelectedEdocs([]);
|
||||
|
||||
// Forzar recarga de documentos
|
||||
const currentPage = edocsPage;
|
||||
setEdocsPage(0);
|
||||
setTimeout(() => setEdocsPage(currentPage), 100);
|
||||
if (edocsPage > lastEdocsPage) {
|
||||
setEdocsPage(lastEdocsPage);
|
||||
} else {
|
||||
setEdocsRefreshKey(prev => prev + 1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showMessage(`Error durante la eliminación: ${error.message}`, 'error');
|
||||
@@ -3074,19 +3138,14 @@ const isEdoc = selectedDocumentForUpload?.tab === 'edoc';
|
||||
const data = await result.json();
|
||||
showMessage(data.message || 'Registro creado exitosamente', 'success');
|
||||
|
||||
// Recargar la tabla correspondiente
|
||||
// Recargar la tabla correspondiente vía refresh keys (los useEffect de cada
|
||||
// pestaña hacen el fetch con la firma y filtros correctos)
|
||||
if (tab === 'partida') {
|
||||
fetchPedimentoPartidas(partidasPage, partidasPageSize, partidasFilters)
|
||||
.then((d) => { setPartidas(d.results || []); setPartidasCount(d.count || 0); })
|
||||
.catch(err => console.error('Error recargando partidas:', err));
|
||||
setPartidasRefreshKey(k => k + 1);
|
||||
} else if (tab === 'cove') {
|
||||
fetchPedimentoCoves && fetchPedimentoCoves(covesPage, covesPageSize, covesFilters)
|
||||
.then((d) => { setCoves(d.results || []); setCovesCount(d.count || 0); })
|
||||
.catch(err => console.error('Error recargando coves:', err));
|
||||
setCovesRefreshKey(k => k + 1);
|
||||
} else if (tab === 'edoc') {
|
||||
fetchPedimentoEdocuments && fetchPedimentoEdocuments(edocsPage, edocsPageSize, edocsFilters)
|
||||
.then((d) => { setEdocs(d.results || []); setEdocsCount(d.count || 0); })
|
||||
.catch(err => console.error('Error recargando edocs:', err));
|
||||
setEdocsRefreshKey(k => k + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`Error: ${error.message}`, 'error');
|
||||
@@ -5156,31 +5215,33 @@ useEffect(() => {
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-gray-700">
|
||||
COVE descargado
|
||||
Estado COVE
|
||||
</label>
|
||||
<select
|
||||
value={covesFilters.cove_descargado}
|
||||
onChange={(e) => setCovesFilters(prev => ({ ...prev, cove_descargado: e.target.value }))}
|
||||
value={covesFilters.cove_estado}
|
||||
onChange={(e) => setCovesFilters(prev => ({ ...prev, cove_estado: 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>
|
||||
<option value="pendiente">Pendiente</option>
|
||||
<option value="descargado">Descargado</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-gray-700">
|
||||
Acuse descargado
|
||||
Estado acuse
|
||||
</label>
|
||||
<select
|
||||
value={covesFilters.acuse_cove_descargado}
|
||||
onChange={(e) => setCovesFilters(prev => ({ ...prev, acuse_cove_descargado: e.target.value }))}
|
||||
value={covesFilters.acuse_cove_estado}
|
||||
onChange={(e) => setCovesFilters(prev => ({ ...prev, acuse_cove_estado: 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>
|
||||
<option value="pendiente">Pendiente</option>
|
||||
<option value="descargado">Descargado</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -5344,22 +5405,10 @@ useEffect(() => {
|
||||
{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>
|
||||
{renderEstadoDescargaBadge(cove.cove_estado, cove.cove_descargado, cove.cove_intentos, cove.ultimo_error)}
|
||||
</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>
|
||||
{renderEstadoDescargaBadge(cove.acuse_cove_estado, cove.acuse_cove_descargado, cove.acuse_cove_intentos, cove.ultimo_error)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
@@ -5663,31 +5712,33 @@ useEffect(() => {
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-gray-700">
|
||||
EDocs descargado
|
||||
Estado EDocs
|
||||
</label>
|
||||
<select
|
||||
value={edocsFilters.edocument_descargado}
|
||||
onChange={(e) => setEdocsFilters(prev => ({ ...prev, edocument_descargado: e.target.value }))}
|
||||
value={edocsFilters.edocument_estado}
|
||||
onChange={(e) => setEdocsFilters(prev => ({ ...prev, edocument_estado: 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>
|
||||
<option value="pendiente">Pendiente</option>
|
||||
<option value="descargado">Descargado</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 text-sm font-medium text-gray-700">
|
||||
Acuse descargado
|
||||
Estado acuse
|
||||
</label>
|
||||
<select
|
||||
value={edocsFilters.acuse_descargado}
|
||||
onChange={(e) => setEdocsFilters(prev => ({ ...prev, acuse_descargado: e.target.value }))}
|
||||
value={edocsFilters.acuse_estado}
|
||||
onChange={(e) => setEdocsFilters(prev => ({ ...prev, acuse_estado: 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>
|
||||
<option value="pendiente">Pendiente</option>
|
||||
<option value="descargado">Descargado</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -5875,22 +5926,10 @@ useEffect(() => {
|
||||
{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>
|
||||
{renderEstadoDescargaBadge(edoc.edocument_estado, edoc.edocument_descargado, edoc.edocument_intentos, edoc.ultimo_error)}
|
||||
</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>
|
||||
{renderEstadoDescargaBadge(edoc.acuse_estado, edoc.acuse_descargado, edoc.acuse_intentos, edoc.ultimo_error)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap" style={{minWidth: '140px'}}>
|
||||
<div className="flex items-center justify-end space-x-2 flex-nowrap">
|
||||
@@ -7335,7 +7374,7 @@ useEffect(() => {
|
||||
<p className="text-sm font-medium text-blue-800">Información</p>
|
||||
<p className="mt-1 text-sm text-blue-700">
|
||||
{activeTab === 'pedimento'
|
||||
? 'Para que los archivos de pedimento completo y/o remesa se vean en este apartado, deben tener la siguiente nomenclatura en el nombre Pedimento Completo ej: vu_PC_26-01-1234-1234567.xml y para Remesa ej: vu_RM_26-01-1234-1234567.xml, El 26-01-1234-1234567 es el pedimento al que se esta subiendo el documento.'
|
||||
? `El sistema detectará si el XML es un Pedimento Completo o una Remesa.`
|
||||
: 'Los archivos se subirán al pedimento actual. Se aceptan múltiples formatos de archivo.' }
|
||||
|
||||
</p>
|
||||
|
||||
@@ -935,21 +935,11 @@ export default function Reports() {
|
||||
}
|
||||
|
||||
setIsExporting(true);
|
||||
|
||||
try {
|
||||
const progressDiv = document.createElement('div');
|
||||
progressDiv.className = 'fixed bottom-4 right-4 bg-white p-4 rounded-lg shadow-xl border border-blue-100';
|
||||
progressDiv.innerHTML = `
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
|
||||
<p class="text-sm text-gray-600">Preparando exportación DataStage...</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(progressDiv);
|
||||
|
||||
try {
|
||||
// DETECCIÓN AUTOMÁTICA DEL MODO
|
||||
const modo = modelosConCampos.length > 1 ? 'multiple' : 'simple';
|
||||
|
||||
|
||||
const exportData = {
|
||||
modo: modo,
|
||||
format: exportFormat,
|
||||
@@ -959,11 +949,10 @@ export default function Reports() {
|
||||
if (modo === 'simple') {
|
||||
// MODO SIMPLE: solo un modelo con campos
|
||||
const modeloUnico = modelosConCampos[0];
|
||||
const modelData = datastageModels.find(m => m.model === modeloUnico);
|
||||
|
||||
|
||||
exportData.model = modeloUnico;
|
||||
exportData.fields = modelFieldsMap[modeloUnico];
|
||||
|
||||
|
||||
} else {
|
||||
// MODO MÚLTIPLE: varios modelos con campos
|
||||
exportData.models = modelosConCampos.map(modelo => {
|
||||
@@ -976,14 +965,8 @@ export default function Reports() {
|
||||
});
|
||||
}
|
||||
|
||||
// Resto del código de exportación...
|
||||
progressDiv.innerHTML = `
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-green-500"></div>
|
||||
<p class="text-sm text-gray-600">Generando archivo DataStage...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// El backend encola la generación en Celery y responde 202 con task_id;
|
||||
// el progreso llega por SSE al TaskProgressCard y ahí se descarga el archivo.
|
||||
const response = await fetchWithAuth(`${API_URL}/reports/exportmodel/datastage/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -997,68 +980,26 @@ export default function Reports() {
|
||||
throw new Error(errorData.error || errorData.message || 'Error al exportar DataStage');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
|
||||
let fileName = '';
|
||||
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="(.+?)"/) ||
|
||||
contentDisposition.match(/filename=([^;]+)/);
|
||||
|
||||
if (filenameMatch) {
|
||||
fileName = filenameMatch[1].replace(/"/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileName) {
|
||||
const isZip = blob.type === 'application/zip';
|
||||
const isExcel = blob.type.includes('spreadsheetml');
|
||||
|
||||
if (isZip) {
|
||||
fileName = modo === 'multiple'
|
||||
? `datastage_reports_${new Date().toISOString().split('T')[0]}.zip`
|
||||
: `datastage_${modelosConCampos[0]}_particionado_${new Date().toISOString().split('T')[0]}.zip`;
|
||||
} else if (isExcel) {
|
||||
fileName = `datastage_${modelosConCampos[0]}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
} else {
|
||||
fileName = `datastage_${modelosConCampos[0]}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.task_id) {
|
||||
addTask({
|
||||
task_id: data.task_id,
|
||||
label: 'Reporte DataStage',
|
||||
organizacion_id: globalFilters.organizacion,
|
||||
taskType: 'report',
|
||||
report_id: data.report_id,
|
||||
status: 'submitted',
|
||||
});
|
||||
pendingReportTasksRef.current.add(data.task_id);
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
showMessage('Reporte solicitado. Puedes ver el progreso en la barra inferior.', 'success');
|
||||
|
||||
progressDiv.innerHTML = `
|
||||
<div class="flex items-center gap-3 text-green-600">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<p class="text-sm">¡Exportación completada! (Modo ${modo})</p>
|
||||
</div>
|
||||
`;
|
||||
setTimeout(() => {
|
||||
progressDiv.style.opacity = '0';
|
||||
progressDiv.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => progressDiv.remove(), 300);
|
||||
}, 2000);
|
||||
|
||||
showMessage(`¡Archivo ${fileName} descargado exitosamente! (${modo === 'multiple' ? 'Múltiple' : 'Simple'})`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ ERROR AL EXPORTAR DATASTAGE:', error);
|
||||
showMessage(error.message || 'Error al exportar DataStage. Por favor intente nuevamente.', 'error');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
const progressDiv = document.querySelector('.fixed.bottom-4.right-4');
|
||||
if (progressDiv) progressDiv.remove();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1795,10 +1736,61 @@ export default function Reports() {
|
||||
<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>{isExporting ? 'Generando archivo...' : `Generar y descargar ${exportFormat.toUpperCase()}`}</span>
|
||||
<span>{isExporting ? 'Generando archivo...' : `Generar reporte ${exportFormat.toUpperCase()}`}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className="mt-10">
|
||||
<h2 className="text-lg font-bold text-slate-700 mb-4">Historial de Reportes</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white rounded-lg shadow border border-slate-200">
|
||||
<thead>
|
||||
<tr className="bg-slate-100">
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">ID</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Estado</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Creado</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Finalizado</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Error</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-600">Descargar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.filter(r => r.report_type === 'datastage').length > 0 ? (
|
||||
reports
|
||||
.filter(r => r.report_type === 'datastage')
|
||||
.map((r) => (
|
||||
<tr key={r.report_id}>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.report_id}</td>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.status}</td>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.created_at}</td>
|
||||
<td className="px-4 py-2 text-xs text-slate-700">{r.finished_at}</td>
|
||||
<td className="px-4 py-2 text-xs text-red-500">{r.error_message ? r.error_message : '-'}</td>
|
||||
<td className="px-4 py-2 text-xs">
|
||||
{r.status === 'ready' ? (
|
||||
<button
|
||||
className="text-blue-600 hover:underline"
|
||||
onClick={() => handleDownloadReport(r.report_id)}
|
||||
>
|
||||
Descargar
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-slate-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-2 text-center text-slate-400">
|
||||
No hay reportes de datastage
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.skeleton-animation {
|
||||
animation: shimmer 2s linear infinite;
|
||||
|
||||
Reference in New Issue
Block a user