Files
frontend/src/pages/TableroAlmacenamiento.jsx

242 lines
9.9 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import fetchWithAuth from '../fetchWithAuth';
import { useNotification } from '../context/NotificationContext';
import { extractApiError } from '../api/apiError';
const initialFilters = {
pedimento_app: '',
aduana: '',
patente: '',
regimen: '',
agente_aduanal: '',
tipo_operacion: '',
fecha_pago_gte: '',
fecha_pago_lte: '',
contribuyente__rfc: '',
};
export default function TableroAlmacenamiento() {
const { showMessage } = useNotification();
const [filters, setFilters] = useState(initialFilters);
const [summary, setSummary] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [reports, setReports] = useState([]);
const handleGenerateReport = async () => {
try {
const params = Object.entries(filters)
.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}` : ''}`;
const res = await fetchWithAuth(url, { method: 'POST' });
if (!res.ok) {
const errMsg = await extractApiError(res);
throw new Error(errMsg);
}
showMessage('Reporte solicitado correctamente. Aparecerá en el historial cuando esté listo.', 'success');
} catch (err) {
showMessage(err.message || 'No se pudo generar el reporte', 'error');
}
};
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();
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();
}
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');
}
};
// Fetch summary data
const fetchSummary = async () => {
setIsLoading(true);
try {
const params = Object.entries(filters)
.filter(([_, v]) => v)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
const url = `${import.meta.env.VITE_EFC_API_URL}/reports/dashboard/summary/${params ? `?${params}` : ''}`;
const res = await fetchWithAuth(url);
if (!res.ok) {
const errMsg = await extractApiError(res);
throw new Error(errMsg);
}
const data = await res.json();
setSummary(data);
} catch (err) {
showMessage(err.message || 'Error al cargar el resumen', 'error');
setSummary(null);
}
setIsLoading(false);
};
// 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) {
showMessage(err.message || 'Error al cargar el historial de reportes', 'error');
setReports([]);
}
};
fetchReports();
}, []);
useEffect(() => {
fetchSummary();
}, []);
const handleFilterChange = (e) => {
setFilters({ ...filters, [e.target.name]: e.target.value });
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100">
{/* Header */}
<div className="bg-slate-800 px-4 py-5 sm:px-6 shadow">
<div className="max-w-7xl mx-auto flex items-center gap-4">
<div className="p-2 bg-blue-700 rounded-xl">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<h1 className="text-2xl font-bold text-white">Resumen de Cumplimiento</h1>
</div>
</div>
</div>
{/* Filtros */}
<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">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
{Object.keys(initialFilters).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={filters[key]}
onChange={handleFilterChange}
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="submit"
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
>
Aplicar Filtros
</button>
<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={handleGenerateReport}
>
Generar Reporte
</button>
</div>
</div>
</form>
</div>
{/* Cards */}
<div className="max-w-7xl mx-auto px-4">
{isLoading ? (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mr-4"></div>
<span className="text-slate-600">Cargando resumen...</span>
</div>
) : summary ? (
<>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
{/* ...Tarjetas existentes... */}
{/* ...aquí van las Card como antes... */}
{/* ...no se repite para brevedad... */}
</div>
{/* Tabla de reportes debajo de las tarjetas */}
<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.length > 0 ? (
reports.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 disponibles.</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</>
) : (
<div className="text-center text-slate-500 py-12">No hay datos para mostrar.</div>
)}
</div>
</div>
);
}