import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTaskProgress } from '../context/TaskProgressContext'; import { getCurrentUser } from '../api/users'; // Helper to get current user with fetchWithAuth const fetchCurrentUserWithAuth = async () => { const url = `${API_URL}/user/users/me/`; const res = await fetchWithAuth(url); if (!res.ok) throw new Error('No se pudo obtener el usuario actual'); return res.json(); }; import { fetchWithAuth } from '../fetchWithAuth'; import { useNotification } from '../context/NotificationContext'; import { extractApiError } from '../api/apiError'; import datastageModelsData from '../data/datastageModels.json'; import pedimentosModelsData from '../data/pedimentosModels.json'; const API_URL = import.meta.env.VITE_EFC_API_URL; // Animaciones const animations = ` @keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes bounce-slow { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-8px) scale(1.05); } } @keyframes pulse-soft { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } } `; // Inyectar estilos de animación if (typeof document !== 'undefined' && !document.getElementById('reports-animations')) { const style = document.createElement('style'); style.id = 'reports-animations'; style.innerHTML = animations; document.head.appendChild(style); } export default function Reports() { // Estado para organizacion_id const [organizacionId, setOrganizacionId] = useState(''); useEffect(() => { async function fetchOrgId() { try { const user = await fetchCurrentUserWithAuth(); if (user && user.organizacion) { setOrganizacionId(user.organizacion); } } catch (err) { setOrganizacionId(''); } } fetchOrgId(); }, []); const { tasks, addTask } = useTaskProgress(); const pendingReportTasksRef = useRef(new Set()); const pollingIntervalRef = useRef(null); // Handler for Generar Reporte in Cumplimiento tab const handleGenerarReporteCumplimiento = async () => { if (!organizacionId) { showMessage('No se pudo obtener el ID de organización. Intenta de nuevo más tarde.', 'warning'); return; } const paramsObj = { ...filtersCumplimiento, organizacion_id: organizacionId }; const params = Object.entries(paramsObj) .filter(([_, v]) => v) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); const url = `${import.meta.env.VITE_EFC_API_URL}/reports/table-summary/${params ? `?${params}` : ''}`; try { const res = await fetchWithAuth(url); if (!res.ok) { const errMsg = await extractApiError(res); throw new Error(errMsg); } const data = await res.json(); if (data.task_id) { addTask({ task_id: data.task_id, label: 'Reporte de Cumplimiento', organizacion_id: organizacionId, taskType: 'report', report_id: data.report_id, status: 'submitted', }); pendingReportTasksRef.current.add(data.task_id); } showMessage('Reporte solicitado. Puedes ver el progreso en la barra inferior.', 'success'); } catch (err) { showMessage(err.message || 'No se pudo generar el reporte', 'error'); } }; // Filtros replicados de TableroAlmacenamiento const initialFiltersCumplimiento = { pedimento_app: '', aduana: '', patente: '', regimen: '', agente_aduanal: '', tipo_operacion: '', fecha_pago_gte: '', fecha_pago_lte: '', contribuyente__rfc: '', }; const [filtersCumplimiento, setFiltersCumplimiento] = useState(initialFiltersCumplimiento); const handleFilterChangeCumplimiento = (e) => { setFiltersCumplimiento({ ...filtersCumplimiento, [e.target.name]: e.target.value }); }; const [summaryData, setSummaryData] = useState(null); const initialFiltersControlPedimento = { pedimento_app: '', fecha_pago__gte: '', fecha_pago__lte: '', organizacion_id: organizacionId || '', }; // control_pedimento const [filtersControlPedimento, setFiltersControlPedimento] = useState(initialFiltersControlPedimento); const handleFilterChangeControlPedimento = (e) => { setFiltersControlPedimento({ ...filtersControlPedimento, [e.target.name]: e.target.value }); }; const handleGenerarReporteControlPedimento = async () => { // if (!organizacionId ) { // alert('No se pudo obtener el organizacion_id. Intenta de nuevo más tarde.'); // return; // } // Build query params from filtersCumplimiento and add organizacion_id const paramsObj = { ...filtersControlPedimento }; if (paramsObj.organizacion_id === '') { showMessage('Selecciona tu organización antes de generar el reporte.', 'warning'); return; } const params = Object.entries(paramsObj) .filter(([_, v]) => v) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); const url = `${import.meta.env.VITE_EFC_API_URL}/reports/control-pedimento/${params ? `?${params}` : ''}`; try { const res = await fetchWithAuth(url); if (!res.ok) { const errMsg = await extractApiError(res); throw new Error(errMsg); } showMessage('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.', 'success'); fetchReports(); startPolling(); } catch (err) { showMessage(err.message || 'No se pudo generar el reporte', 'error'); } }; // Fetch summary data for dashboard/cards const fetchSummary = async () => { try { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/dashboard/summary/`; const res = await fetch(url, { method: 'GET', credentials: 'include', mode: 'cors', headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}`, 'Accept': '*/*', }, referrer: window.location.href, }); if (!res.ok) throw new Error('Error al obtener el resumen'); const data = await res.json(); setSummaryData(data); } catch (err) { setSummaryData(null); } }; const [reports, setReports] = useState([]); // Leer DEBUG_MODE desde variables de entorno const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true'; const { showMessage } = useNotification(); const handleDownloadReport = async (reportId) => { try { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-download/${reportId}/`; const res = await fetchWithAuth(url); if (!res.ok) { const errMsg = await extractApiError(res); throw new Error(errMsg); } const blob = await res.blob(); const disposition = res.headers.get('Content-Disposition'); let filename = ''; if (disposition) { const match = disposition.match(/filename="?([^";\s]+)"?/); if (match) filename = match[1]; } if (!filename) { const isXlsx = blob.type.includes('spreadsheetml') || blob.type.includes('openxmlformats'); filename = `reporte_${reportId}.${isXlsx ? 'xlsx' : 'csv'}`; } const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (err) { showMessage(err.message || 'No se pudo descargar el reporte', 'error'); } }; const [isExporting, setIsExporting] = useState(false); const [exportFormat, setExportFormat] = useState('excel'); const [showExportSuccess, setShowExportSuccess] = useState(false); const [showHelp, setShowHelp] = useState(false); const [showTour, setShowTour] = useState(false); const [tourStep, setTourStep] = useState(0); const [organizaciones, setOrganizaciones] = useState([]); const [importadores, setImportadores] = useState([]); const [rfcOptions, setRfcOptions] = useState([]); const [rfcsCumplimiento, setRfcsCumplimiento] = useState([]); useEffect(() => { const fetchOrganizaciones = async () => { try { const url = `${import.meta.env.VITE_EFC_API_URL}/organization/organizaciones/`; const res = await fetchWithAuth(url); // ← USA fetchWithAuth if (!res.ok) throw new Error('Error al obtener las organizaciones'); const data = await res.json(); setOrganizaciones(data); } catch (err) { console.error('Error fetching organizaciones:', err); setOrganizaciones([]); // ← Asegurar que siempre sea un array } }; fetchOrganizaciones(); }, []); useEffect(() => { const fetchImportadores = async () => { try { const res = await fetch( `${import.meta.env.VITE_EFC_API_URL}/customs/importadores/`, { method: 'GET', headers: { 'Authorization': `Bearer ${localStorage.getItem('access')}` } }); const data = await res.json(); setImportadores(data); } catch { console.error('Error fetching importadores:', err); setImportadores([]); } }; fetchImportadores(); }, []); useEffect(() => { if (!organizacionId) return; const load = async () => { try { const url = `${API_URL}/reports/exportmodel/datastage/?organizacion=${organizacionId}`; const res = await fetchWithAuth(url); if (!res.ok) return; const data = await res.json(); setRfcsCumplimiento(data.rfcs || []); } catch { setRfcsCumplimiento([]); } }; load(); }, [organizacionId]); const [globalFilters, setGlobalFilters] = useState({ rfc: '', fecha_pago_desde: '', fecha_pago_hasta: '', organizacion: '', patente: '', pedimento: '' }); // Cargar RFCs cuando cambia la organización seleccionada en filtros globales useEffect(() => { const fetchRfcs = async () => { if (!globalFilters.organizacion) { setRfcOptions([]); return; } try { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/exportmodel/datastage/?organizacion=${globalFilters.organizacion}`; const res = await fetchWithAuth(url); if (!res.ok) throw new Error('Error al obtener RFCs'); const data = await res.json(); setRfcOptions(data.rfcs || []); } catch (err) { console.error('Error fetching RFCs:', err); setRfcOptions([]); } }; fetchRfcs(); }, [globalFilters.organizacion]); const renderGlobalFilters = () => (

Filtros globales

Filtros aplicables a todos los modelos

{/* Filtro por Organización */}
{/* Filtro por RFC */}
{/* modificar de input a select */}
{/* setGlobalFilters(prev => ({ ...prev, rfc: e.target.value }))} className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-all duration-200 bg-white" placeholder="Ej: ABC123456789" /> */}
{/* Filtro por Fecha Pago Desde */}
setGlobalFilters(prev => ({ ...prev, fecha_pago_desde: e.target.value }))} className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-all duration-200 bg-white" />
{/* Filtro por Fecha Pago Hasta */}
setGlobalFilters(prev => ({ ...prev, fecha_pago_hasta: e.target.value }))} className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-all duration-200 bg-white" />
{/* Filtro por Patente */}
setGlobalFilters(prev => ({ ...prev, patente: e.target.value }))} className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-all duration-200 bg-white" placeholder="Ej: 1234" />
{/* Filtro por Pedimento */}
setGlobalFilters(prev => ({ ...prev, pedimento: e.target.value }))} className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-all duration-200 bg-white" placeholder="Ej: 1234567" />
{/* Botón para limpiar filtros globales */}
); // Estado para formato de exportación personalizado const [showFormatSelector, setShowFormatSelector] = useState(false); // Estado para pestañas — persiste en URL (?tab=...) const VALID_TABS = ['pedimentos', 'control_pedimentos', 'datastage', 'Cumplimiento', 'coves']; const [activeTab, setActiveTab] = useState(() => { const params = new URLSearchParams(window.location.search); const tab = params.get('tab'); return VALID_TABS.includes(tab) ? tab : 'pedimentos'; }); useEffect(() => { const params = new URLSearchParams(window.location.search); params.set('tab', activeTab); history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`); }, [activeTab]); // Mostrar Cumplimiento en producción: eliminar lógica que oculta la pestaña // Importar modelos const datastageModels = datastageModelsData?.models || []; const pedimentosModels = pedimentosModelsData?.models || []; // Obtener el modelo inicial según la pestaña activa const initialModels = activeTab === 'pedimentos' ? pedimentosModels : datastageModels; const defaultModel = initialModels[0] || { model: '', fields: [], filters: {} }; // esquema para el resto // Estado para modelo seleccionado const [selectedModel, setSelectedModel] = useState(defaultModel.model); // Estado para campos seleccionados const [selectedFields, setSelectedFields] = useState(defaultModel.fields); // esquema para el nuevo const [modoMultiple, setModoMultiple] = useState(false); const [selectedModels, setSelectedModels] = useState(defaultModel.model); const [selectedFieldsDataStage, setSelectedFieldsDataStage] = useState([]); const [modelFieldsMap, setModelFieldsMap] = useState({}); const isModoMultiple = Object.keys(modelFieldsMap).length > 1; useEffect(() => { if (selectedModel) { // Cargar campos previamente seleccionados para este modelo const camposGuardados = modelFieldsMap[selectedModel] || []; setSelectedFieldsDataStage(camposGuardados); } }, [selectedModel, modelFieldsMap]); // Estado para campo seleccionado en lista disponible const [availableSelected, setAvailableSelected] = useState(null); // Estado para los filtros const [filters, setFilters] = useState(defaultModel.filters); // Actualizar campos seleccionados al cambiar de modelo o pestaña React.useEffect(() => { const models = activeTab === 'pedimentos' ? pedimentosModels : datastageModels; // Al cambiar de pestaña, seleccionar el primer modelo de la pestaña actual const newModel = models[0]?.model || ''; setSelectedModel(newModel); const modelObj = models.find(m => m.model === newModel); if (modelObj) { setSelectedFields(modelObj.fields); setAvailableSelected(null); setFilters(modelObj.filters); } }, [activeTab]); // Efecto separado para cambios de modelo dentro de la misma pestaña React.useEffect(() => { const models = activeTab === 'pedimentos' ? pedimentosModels : datastageModels; const modelObj = models.find(m => m.model === selectedModel); if (modelObj) { setSelectedFields(modelObj.fields); setAvailableSelected(null); setFilters(modelObj.filters); } }, [selectedModel]); // Obtener los modelos según la pestaña activa const models = activeTab === 'pedimentos' ? pedimentosModels : datastageModels; // Encontrar el modelo actual dentro de los modelos de la pestaña activa const currentModel = models.find(m => m.model === selectedModel) || defaultModel; // Campos disponibles (no seleccionados) del modelo actual const availableFields = currentModel.fields.filter(f => !selectedFields.includes(f)); // Mover campo de disponible a seleccionado const addField = (field) => { setSelectedFields([...selectedFields, field]); setAvailableSelected(null); }; // Mover campo de seleccionado a disponible const removeField = (field) => { setSelectedFields(selectedFields.filter(f => f !== field)); }; // Incluir todos los campos const includeAllFields = () => { setSelectedFields([...currentModel.fields]); setAvailableSelected(null); }; // Renderizar selector dual de campos // Estado para campo seleccionado en lista de incluidos const [includedSelected, setIncludedSelected] = useState(null); // Quitar todos los campos const removeAllFields = () => { setSelectedFields([]); setIncludedSelected(null); }; // Quitar campo seleccionado en incluidos const removeSelectedField = () => { if (includedSelected) { removeField(includedSelected); setIncludedSelected(null); } }; // Tour steps for guided experience const tourSteps = [ { target: '.tab-selector', content: '🎯 Comienza seleccionando el tipo de reporte que deseas generar', position: 'bottom' }, { target: '.model-selector', content: '📊 Si elegiste Datastage, selecciona el modelo específico aquí', position: 'bottom' }, { target: '.format-selector', content: '📁 Elige el formato de tu archivo: Excel para análisis avanzado o CSV para compatibilidad', position: 'bottom' }, { target: '.fields-selector', content: '✨ Selecciona los campos que quieres incluir en tu reporte. Puedes agregar o quitar campos fácilmente', position: 'top' }, { target: '.filters-section', content: '🔍 Aplica filtros para obtener exactamente los datos que necesitas', position: 'top' }, { target: '.export-button', content: '🚀 ¡Listo! Haz clic aquí para generar y descargar tu reporte', position: 'top' } ]; const startTour = () => { setShowTour(true); setTourStep(0); }; const nextTourStep = () => { if (tourStep < tourSteps.length - 1) { setTourStep(tourStep + 1); } else { setShowTour(false); setTourStep(0); } }; const skipTour = () => { setShowTour(false); setTourStep(0); }; const fetchReports = useCallback(async () => { try { const url = `${import.meta.env.VITE_EFC_API_URL}/reports/report-document-list/`; const res = await fetchWithAuth(url); if (!res.ok) throw new Error('Error al obtener el historial de reportes'); const data = await res.json(); setReports(data); } catch { setReports([]); } }, []); const stopPolling = useCallback(() => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }, []); const startPolling = useCallback(() => { if (pollingIntervalRef.current) return; pollingIntervalRef.current = setInterval(fetchReports, 5000); }, [fetchReports]); useEffect(() => { fetchReports(); }, [fetchReports]); // Detener polling cuando todos los reportes de control_pedimento lleguen a estado terminal useEffect(() => { const hasPending = reports .filter(r => r.report_type === 'control_pedimento') .some(r => r.status !== 'ready' && r.status !== 'error'); if (!hasPending) stopPolling(); }, [reports, stopPolling]); // Limpiar intervalo al desmontar useEffect(() => () => stopPolling(), [stopPolling]); // Refrescar historial cuando completa una tarea de reporte de cumplimiento useEffect(() => { if (!tasks || pendingReportTasksRef.current.size === 0) return; let changed = false; tasks.forEach(t => { if ( pendingReportTasksRef.current.has(t.task_id) && (t.status === 'completed' || t.status === 'failed') ) { pendingReportTasksRef.current.delete(t.task_id); changed = true; } }); if (changed) fetchReports(); }, [tasks, fetchReports]); useEffect(() => { fetchSummary(); }, []); // Función para manejar la exportación del modelo const handleExportModel = async () => { if (selectedFields.length === 0) { showMessage('Por favor selecciona al menos un campo para exportar', 'error'); return; } setIsExporting(true); try { // Mostrar indicador de progreso 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 = `

Preparando exportación...

`; document.body.appendChild(progressDiv); // Construir filtros no vacíos const nonEmptyFilters = Object.entries(filters) .reduce((acc, [key, value]) => { if (value?.trim()) { acc[key] = value.trim(); } return acc; }, {}); // Construir payload const payload = { model: currentModel.model, module: currentModel.module, fields: selectedFields, type: exportFormat, ...(Object.keys(nonEmptyFilters).length > 0 && { filters: nonEmptyFilters }), }; // Realizar la petición const response = await fetchWithAuth(`${API_URL}/reports/exportmodel/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || errorData.message || 'Error al exportar el modelo'); } // Actualizar mensaje de progreso progressDiv.innerHTML = `

Preparando archivo...

`; const blob = await response.blob(); const contentType = exportFormat === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'text/csv'; const url = window.URL.createObjectURL(new Blob([blob], { type: contentType })); const link = document.createElement('a'); const extension = exportFormat === 'excel' ? 'xlsx' : 'csv'; const fileName = `${currentModel.model}_${new Date().toISOString().split('T')[0]}.${extension}`; link.href = url; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); // Mostrar mensaje de éxito progressDiv.innerHTML = `

¡Exportación completada!

`; setTimeout(() => { progressDiv.style.opacity = '0'; progressDiv.style.transform = 'translateY(100%)'; setTimeout(() => progressDiv.remove(), 300); }, 2000); showMessage(`¡Archivo ${fileName} descargado exitosamente!`, 'success'); setShowExportSuccess(true); } catch (error) { console.error('Error al exportar:', error); showMessage(error.message || 'Error al exportar el modelo. Por favor intente nuevamente.', 'error'); } finally { setIsExporting(false); // Remove progress indicator if it exists const progressDiv = document.querySelector('.fixed.bottom-4.right-4'); if (progressDiv) progressDiv.remove(); } }; // Modificar la función addField para actualizar el mapeo const addFieldDataStage = (field) => { const nuevosCampos = [...selectedFieldsDataStage, field]; // Actualizar estado local setSelectedFieldsDataStage(nuevosCampos); // Actualizar mapeo global setModelFieldsMap(prev => ({ ...prev, [selectedModel]: nuevosCampos })); }; // Modificar la función removeField para actualizar el mapeo const removeFieldDataStage = (field) => { const nuevosCampos = selectedFieldsDataStage.filter(f => f !== field); // Actualizar estado local setSelectedFieldsDataStage(nuevosCampos); // Actualizar mapeo global setModelFieldsMap(prev => ({ ...prev, [selectedModel]: nuevosCampos })); }; // Modificar includeAllFields const includeAllFieldsDataStage = () => { const currentModel = datastageModels.find(m => m.model === selectedModel); if (!currentModel) return; setSelectedFieldsDataStage([...currentModel.fields]); setModelFieldsMap(prev => ({ ...prev, [selectedModel]: [...currentModel.fields] })); }; // Modificar removeAllFields const removeAllFieldsDataStage = () => { setSelectedFieldsDataStage([]); setModelFieldsMap(prev => ({ ...prev, [selectedModel]: [] })); }; // Nueva función específica para DataStage múltiple const handleExportDataStage = async () => { // Verificar que haya al menos un modelo con campos seleccionados const modelosConCampos = Object.entries(modelFieldsMap) .filter(([_, campos]) => campos.length > 0) .map(([modelo]) => modelo); if (modelosConCampos.length === 0) { showMessage('Por favor selecciona al menos un campo en algún modelo', 'error'); return; } if (!globalFilters.organizacion) { showMessage('Debes seleccionar una organización antes de generar el reporte', 'error'); return; } setIsExporting(true); try { // DETECCIÓN AUTOMÁTICA DEL MODO const modo = modelosConCampos.length > 1 ? 'multiple' : 'simple'; const exportData = { modo: modo, format: exportFormat, globalFilters: globalFilters }; if (modo === 'simple') { // MODO SIMPLE: solo un modelo con campos const modeloUnico = modelosConCampos[0]; exportData.model = modeloUnico; exportData.fields = modelFieldsMap[modeloUnico]; } else { // MODO MÚLTIPLE: varios modelos con campos exportData.models = modelosConCampos.map(modelo => { const modelData = datastageModels.find(m => m.model === modelo); return { model: modelo, name: modelData?.name || modelo, fields: modelFieldsMap[modelo] }; }); } // 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: { 'Content-Type': 'application/json', }, body: JSON.stringify(exportData), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || errorData.message || 'Error al exportar DataStage'); } 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); } showMessage('Reporte solicitado. Puedes ver el progreso en la barra inferior.', '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 renderFieldsDataStage = () => { const currentModel = datastageModels.find(m => m.model === selectedModel); const availableFields = currentModel ? currentModel.fields : []; // 🔥 CORREGIR: Siempre usar selectedFieldsDataStage para el modelo actual const selectedFieldsForModel = selectedFieldsDataStage; const formatFieldName = (field) => { return field.split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; return (

{modoMultiple ? `Campos de ${selectedModel}` : 'Campos del reporte'}

{modoMultiple ? `Selecciona campos para ${selectedModel}` : 'Selecciona los campos a incluir' }

{selectedFieldsForModel.length} seleccionados
{/* Panel de campos */}
{/* Panel izquierdo - Campos disponibles */}

Campos disponibles

{availableFields.filter(field => !selectedFieldsForModel.includes(field)).length === 0 ? (

Todos los campos están incluidos

) : (
{availableFields .filter(field => !selectedFieldsForModel.includes(field)) .map(field => (
addFieldDataStage(field)} className="group flex items-center justify-between p-2 rounded-md cursor-pointer transition-all duration-200 hover:bg-gray-100" > {formatFieldName(field)}
))}
)}
{/* Panel derecho - Campos seleccionados */}

{modoMultiple ? `Campos incluidos en ${selectedModel}` : 'Campos incluidos'}

{selectedFieldsForModel.length === 0 ? (

No hay campos seleccionados

) : (
{selectedFieldsForModel.map(field => (
removeFieldDataStage(field)} > {formatFieldName(field)} {modoMultiple && ( ({selectedModel}) )}
))}
)}
); }; const renderFields = () => { const formatFieldName = (field) => { return field.split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; return (
{/* Header con búsqueda y contador */}

Campos del reporte

Selecciona los campos a incluir

{selectedFields.length} seleccionados
{/* Panel de campos */}
{/* Panel izquierdo - Campos disponibles */}

Campos disponibles

{availableFields.length === 0 ? (

Todos los campos están incluidos

) : (
{availableFields.map(field => (
setAvailableSelected(field)} onDoubleClick={() => addField(field)} className={`group flex items-center justify-between p-2 rounded-md cursor-pointer transition-all duration-200 ${availableSelected === field ? 'bg-blue-50 text-blue-700' : 'hover:bg-gray-100' }`} > {formatFieldName(field)}
))}
)}
{/* Panel derecho - Campos seleccionados */}

Campos incluidos

{selectedFields.length === 0 ? (

No hay campos seleccionados

) : (
{selectedFields.map(field => (
setIncludedSelected(field)} onDoubleClick={() => removeField(field)} > {formatFieldName(field)}
))}
)}
); }; const renderFilters = () => (

Filtros de búsqueda

Refina los resultados del reporte

{Object.entries(currentModel.filters).map(([key, value]) => { const label = key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); const isDate = key.toLowerCase().includes('fecha'); return (
setFilters(prev => ({ ...prev, [key]: e.target.value }))} className="block w-full rounded-lg border-gray-300 pl-3 pr-10 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-all duration-200 bg-white" placeholder={`Buscar por ${label.toLowerCase()}`} />
{isDate ? ( ) : ( )}
); })}
); // Contenido de cada pestaña const tabContents = { pedimentos: (

Generar reporte de pedimentos

Selecciona los campos y ajusta los filtros para generar el reporte.

Campos

{renderFields()}

Filtros

{renderFilters()}
), control_pedimentos: (

Generar reporte de Control de Requerimiento

Aquí puedes generar y descargar el reporte de Control de Requerimiento.

{/* Filtros replicados */}
{ e.preventDefault(); fetchSummary(); }} className="bg-white rounded-lg shadow-sm border border-slate-200 p-4">
{Object.keys(initialFiltersControlPedimento).map((key) => (
{key === 'organizacion_id' ? ( ) : ( )}
))}
{/* Aquí va la lógica y UI específica para Cumplimiento */} {/* Tabla de reportes debajo de las tarjetas */}

Historial de Reportes

{reports.filter(r => r.report_type === 'control_pedimento').length > 0 ? ( reports .filter(r => r.report_type === 'control_pedimento') .map((r) => ( )) ) : ( )}
ID Estado Creado Finalizado Error Descargar
{r.report_id} {r.status} {r.created_at} {r.finished_at} {r.error_message ? r.error_message : '-'} {r.status === 'ready' ? ( ) : ( - )}
{reports.length > 0 ? 'No hay reportes de control de pedimento' : 'No hay reportes disponibles'}
), datastage: (

Generar reporte de datastage

Selecciona el modelo, revisa los campos y ajusta los filtros para generar el reporte.

Campos

{renderFieldsDataStage()}
Busqueda por modelo: {isModoMultiple ? 'MÚLTIPLES MODELOS' : 'SINGULAR'}
{Object.keys(modelFieldsMap).filter(model => modelFieldsMap[model]?.length > 0).length} modelo(s) con campos
{/* Mostrar modelos activos */} {Object.keys(modelFieldsMap).filter(model => modelFieldsMap[model]?.length > 0).length > 0 && (
Modelos activos: {Object.keys(modelFieldsMap) .filter(model => modelFieldsMap[model]?.length > 0) .map(model => `${model} (${modelFieldsMap[model].length} campos)`) .join(', ')}
)}

Filtros Globales

{renderGlobalFilters()}
{/*

Filtros

{renderFilters()}
*/}

Historial de Reportes

{reports.filter(r => r.report_type === 'datastage').length > 0 ? ( reports .filter(r => r.report_type === 'datastage') .map((r) => ( )) ) : ( )}
ID Estado Creado Finalizado Error Descargar
{r.report_id} {r.status} {r.created_at} {r.finished_at} {r.error_message ? r.error_message : '-'} {r.status === 'ready' ? ( ) : ( - )}
No hay reportes de datastage
), Cumplimiento: (

Generar reporte de Cumplimiento

Aquí puedes generar y descargar el reporte de Cumplimiento.

e.preventDefault()} className="bg-white rounded-lg shadow-sm border border-slate-200 p-4">
{/* Organización — solo lectura */}
o.id === organizacionId)?.nombre || organizacionId || ''} className="w-full border border-slate-200 rounded px-2 py-1 text-sm bg-slate-50 text-slate-500 cursor-not-allowed" />
{/* RFC Contribuyente — dropdown dinámico */}
{/* Pedimento App */}
{/* Aduana */}
{/* Patente */}
{/* Régimen */}
{/* Agente Aduanal */}
{/* Tipo Operación */}
{/* Fecha Pago Desde */}
{/* Fecha Pago Hasta */}
{/* Aquí va la lógica y UI específica para Cumplimiento */} {/* Tabla de reportes debajo de las tarjetas */}

Historial de Reportes

{reports.filter(r => r.report_type === 'cumplimiento').length > 0 ? ( reports .filter(r => r.report_type === 'cumplimiento') .map((r) => ( )) ) : ( )}
ID Estado Creado Finalizado Error Descargar
{r.report_id} {r.status} {r.created_at} {r.finished_at} {r.error_message ? r.error_message : '-'} {r.status === 'ready' ? ( ) : ( - )}
{reports.length > 0 ? 'No hay reportes de cumplimiento' : 'No hay reportes disponibles'}
), coves: (

Generar reporte de COVES

Aquí puedes generar y descargar el reporte de COVES.

{/* Aquí va la lógica y UI específica para COVES */}
), }; return (
{/* Header mejorado y decorativo */}

Centro de Reportes

Consulta, genera y descarga reportes relacionados con el sistema aduanero

{/* Botones de ayuda */}
{/* Efectos decorativos de fondo */}
{/* Partículas flotantes */}
{/* Panel de ayuda */} {showHelp && (

Guía de uso del Centro de Reportes

🎯 Paso 1: Selecciona el tipo de reporte

Elige entre Pedimentos cargados para documentos de importación/exportación o Datastage cargados para datos procesados del sistema.

📊 Paso 2: Configura tu reporte

Selecciona el formato (Excel para análisis o CSV para compatibilidad), elige los campos que necesitas y aplica filtros específicos.

🚀 Paso 3: Genera y descarga

Haz clic en el botón de exportación y el archivo se descargará automáticamente a tu dispositivo.

💡 Consejos útiles

  • Usa Excel para reportes con gráficos y análisis avanzado
  • Usa CSV para importar datos en otros sistemas
  • Aplica filtros para reducir el tamaño del archivo
  • Selecciona solo los campos que realmente necesitas
)} {/* Pestañas */}
{isDebugMode && ( )}
{/* Contenido de la pestaña activa */}
{tabContents[activeTab]}
{/* Tour Overlay */}
); } // Tour overlay component const TourOverlay = ({ show, step, steps, onNext, onSkip }) => { if (!show || !steps[step]) return null; return (
{step + 1}

Paso {step + 1} de {steps.length}

{steps[step].content}

); };