feature/implementacion de hub en EFC

This commit is contained in:
2026-06-08 07:19:29 -06:00
parent 6e2634d11b
commit 1c6b07be58
23 changed files with 2178 additions and 1039 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
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 () => {
@@ -62,13 +63,16 @@ export default function Reports() {
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;
}
// Build query params from filtersCumplimiento and add organizacion_id
const paramsObj = { ...filtersCumplimiento, organizacion_id: organizacionId };
const params = Object.entries(paramsObj)
.filter(([_, v]) => v)
@@ -81,7 +85,19 @@ export default function Reports() {
const errMsg = await extractApiError(res);
throw new Error(errMsg);
}
showMessage('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.', 'success');
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');
}
@@ -142,6 +158,8 @@ export default function Reports() {
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');
}
@@ -182,10 +200,15 @@ export default function Reports() {
throw new Error(errMsg);
}
const blob = await res.blob();
let filename = `reporte_${reportId}.csv`;
const disposition = res.headers.get('Content-Disposition');
if (disposition && disposition.includes('filename=')) {
filename = disposition.split('filename=')[1].replace(/"/g, '').trim();
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);
@@ -208,6 +231,7 @@ export default function Reports() {
const [organizaciones, setOrganizaciones] = useState([]);
const [importadores, setImportadores] = useState([]);
const [rfcOptions, setRfcOptions] = useState([]);
const [rfcsCumplimiento, setRfcsCumplimiento] = useState([]);
useEffect(() => {
const fetchOrganizaciones = async () => {
@@ -242,6 +266,22 @@ export default function Reports() {
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: '',
@@ -493,8 +533,19 @@ export default function Reports() {
// Estado para formato de exportación personalizado
const [showFormatSelector, setShowFormatSelector] = useState(false);
// Estado para pestañas
const [activeTab, setActiveTab] = useState('pedimentos');
// 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
@@ -655,22 +706,59 @@ export default function Reports() {
setTourStep(0);
};
// Fetch report list from API
useEffect(() => {
const fetchReports = 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 (err) {
setReports([]);
}
};
fetchReports();
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();
}, []);
@@ -1727,39 +1815,144 @@ export default function Reports() {
<div className="p-6">
<h2 className="text-xl font-bold mb-2 text-blue-900">Generar reporte de Cumplimiento</h2>
<p className="mb-4 text-gray-700">Aquí puedes generar y descargar el reporte de Cumplimiento.</p>
{/* Filtros replicados */}
<div className="max-w-7xl mx-auto mt-6 mb-4 px-4">
<form onSubmit={(e) => {
e.preventDefault();
fetchSummary();
}} className="bg-white rounded-lg shadow-sm border border-slate-200 p-4">
<form onSubmit={(e) => e.preventDefault()} className="bg-white rounded-lg shadow-sm border border-slate-200 p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
{Object.keys(initialFiltersCumplimiento).map((key) => (
<div key={key}>
<label className="block text-xs font-medium text-slate-600 mb-1" htmlFor={key}>
{key.replace(/_/g, ' ').replace('gte', 'desde').replace('lte', 'hasta')}
</label>
<input
type={key.includes('fecha') ? 'date' : 'text'}
name={key}
id={key}
value={filtersCumplimiento[key]}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
))}
{/* Organización — solo lectura */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Organización</label>
<input
type="text"
readOnly
value={organizaciones?.results?.find(o => 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"
/>
</div>
{/* RFC Contribuyente — dropdown dinámico */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">RFC Contribuyente</label>
<select
name="contribuyente__rfc"
value={filtersCumplimiento.contribuyente__rfc}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500 bg-white"
>
<option value="">Todos los RFC</option>
<option value="SIN_RFC">Pedimentos sin RFC</option>
{rfcsCumplimiento.map(rfc => (
<option key={rfc} value={rfc}>{rfc}</option>
))}
</select>
</div>
{/* Pedimento App */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Pedimento</label>
<input
type="text"
name="pedimento_app"
value={filtersCumplimiento.pedimento_app}
onChange={handleFilterChangeCumplimiento}
placeholder="Ej. 21-160-3910-0003357"
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Aduana */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Aduana</label>
<input
type="text"
name="aduana"
value={filtersCumplimiento.aduana}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Patente */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Patente</label>
<input
type="text"
name="patente"
value={filtersCumplimiento.patente}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Régimen */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Régimen</label>
<input
type="text"
name="regimen"
value={filtersCumplimiento.regimen}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Agente Aduanal */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Agente Aduanal</label>
<input
type="text"
name="agente_aduanal"
value={filtersCumplimiento.agente_aduanal}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Tipo Operación */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Tipo Operación</label>
<input
type="text"
name="tipo_operacion"
value={filtersCumplimiento.tipo_operacion}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Fecha Pago Desde */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Fecha pago desde</label>
<input
type="date"
name="fecha_pago_gte"
value={filtersCumplimiento.fecha_pago_gte}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* Fecha Pago Hasta */}
<div>
<label className="block text-xs font-medium text-slate-600 mb-1">Fecha pago hasta</label>
<input
type="date"
name="fecha_pago_lte"
value={filtersCumplimiento.fecha_pago_lte}
onChange={handleFilterChangeCumplimiento}
className="w-full border border-slate-300 rounded px-2 py-1 text-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div className="flex justify-end">
<div className="flex gap-2">
<button
type="button"
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"
onClick={handleGenerarReporteCumplimiento}
>
Generar Reporte
</button>
</div>
<button
type="button"
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"
onClick={handleGenerarReporteCumplimiento}
>
Generar Reporte
</button>
</div>
</form>
</div>