From 5387eb25cffe9a3c3b9994ea6e4e38f539285aab Mon Sep 17 00:00:00 2001 From: Dulce Date: Tue, 25 Nov 2025 13:25:29 -0700 Subject: [PATCH] reportes update --- src/pages/PedimentoDetail.jsx | 11 +- src/pages/Reports.jsx | 853 ++++++++++++++++++++++++++++++++-- 2 files changed, 831 insertions(+), 33 deletions(-) diff --git a/src/pages/PedimentoDetail.jsx b/src/pages/PedimentoDetail.jsx index 2277e32..4c24fe8 100644 --- a/src/pages/PedimentoDetail.jsx +++ b/src/pages/PedimentoDetail.jsx @@ -4421,7 +4421,8 @@ export default function PedimentoDetail() { ) : previewType === 'xml' ? ( -
+ //
+
@@ -4445,10 +4446,14 @@ export default function PedimentoDetail() {
diff --git a/src/pages/Reports.jsx b/src/pages/Reports.jsx
index 13dca25..2037025 100644
--- a/src/pages/Reports.jsx
+++ b/src/pages/Reports.jsx
@@ -94,11 +94,9 @@ export default function Reports() {
       .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
       .join('&');
     const url = `${import.meta.env.VITE_EFC_API_URL}/reports/table-summary/${params ? `?${params}` : ''}`;
-    console.log('Cumplimiento Report Request:', url);
     try {
-  const res = await fetchWithAuth(url);
+      const res = await fetchWithAuth(url);
       const data = await res.json();
-      console.log('Cumplimiento Report Response:', data);
       if (!res.ok) throw new Error('Error al generar el reporte');
       alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.');
     } catch (err) {
@@ -123,6 +121,47 @@ export default function Reports() {
   };
   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 == ''){
+      alert('No se pudo obtener el organizacion_id. Selecciona tu organizacion para intenta de nuevo.');
+      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);
+      const data = await res.json();
+      if (!res.ok) throw new Error('Error al generar el reporte');
+      alert('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.');
+    } catch (err) {
+      alert('No se pudo generar el reporte.');
+    }
+  };
+
   // Fetch summary data for dashboard/cards
   const fetchSummary = async () => {
     try {
@@ -156,6 +195,233 @@ export default function Reports() {
   const [showTour, setShowTour] = useState(false);
   const [tourStep, setTourStep] = useState(0);
 
+  const [organizaciones, setOrganizaciones] = 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();
+  }, []);
+
+  const [globalFilters, setGlobalFilters] = useState({
+    rfc: '',
+    fecha_pago_desde: '',
+    fecha_pago_hasta: '',
+    organizacion: '',
+    patente: '',
+    pedimento: ''
+  });
+
+  const renderGlobalFilters = () => (
+    
+
+
+
+
+ + + +
+
+

Filtros globales

+

Filtros aplicables a todos los modelos

+
+
+
+
+
+ {/* Filtro por RFC */} +
+ +
+ 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" + /> +
+ + + +
+
+
+ + {/* Filtro por Organización */} +
+ +
+ +
+ + + +
+
+
+
+ + {/* Botón para limpiar filtros globales */} +
+ +
+
+
+
+ ); + // Estado para formato de exportación personalizado const [showFormatSelector, setShowFormatSelector] = useState(false); @@ -172,11 +438,27 @@ export default function Reports() { 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); @@ -324,6 +606,7 @@ export default function Reports() { useEffect(() => { fetchSummary(); }, []); + // Función para manejar la exportación del modelo const handleExportModel = async () => { if (selectedFields.length === 0) { @@ -429,6 +712,353 @@ export default function Reports() { } }; + // 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) { + alert('Por favor selecciona al menos un campo en algún modelo'); + return; + } + + 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 = ` +
+
+

Preparando exportación DataStage...

+
+ `; + document.body.appendChild(progressDiv); + + // 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]; + 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 => { + const modelData = datastageModels.find(m => m.model === modelo); + return { + model: modelo, + name: modelData?.name || modelo, + fields: modelFieldsMap[modelo] + }; + }); + } + + // Resto del código de exportación... + progressDiv.innerHTML = ` +
+
+

Generando archivo DataStage...

+
+ `; + + 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 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 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); + + progressDiv.innerHTML = ` +
+ + + +

¡Exportación completada! (Modo ${modo})

+
+ `; + 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(); + } + }; + + 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('_') @@ -752,6 +1382,118 @@ export default function Reports() {
), + 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) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
IDEstadoCreadoFinalizadoErrorDescargar
{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: (
@@ -827,14 +1569,43 @@ export default function Reports() {

Campos

- {renderFields()} + {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()} -
+
*/} - ) : ( - - - )} - - - )) + {reports.filter(r => r.report_type === 'cumplimiento').length > 0 ? ( + reports + .filter(r => r.report_type === 'cumplimiento') + .map((r) => ( + + {r.report_id} + {r.status} + {r.created_at} + {r.finished_at} + {r.error_message ? r.error_message : '-'} + + {r.status === 'ready' ? ( + + ) : ( + - + )} + + + )) ) : ( - No hay reportes disponibles. + + {reports.length > 0 ? 'No hay reportes de cumplimiento' : 'No hay reportes disponibles'} + )} @@ -1105,6 +1880,24 @@ export default function Reports() { Pedimentos cargados
+ + {isDebugMode && ( + + )} +