feature/nueva funcionalidad al hacer login mediante HUB y nuevos reportes en segundo plano

This commit is contained in:
2026-06-15 11:22:26 -06:00
parent 45548f9bc8
commit 14db21c671
6 changed files with 220 additions and 188 deletions

View File

@@ -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));

View File

@@ -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));

View File

@@ -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,

View File

@@ -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');

View File

@@ -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>

View File

@@ -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;