feature/implementacion de hub en EFC
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user