Continuación de CRUDs e inicio de Reportes.
This commit is contained in:
8
Frontend/src/models/dtos/Reportes/ExistenciaPapelDto.ts
Normal file
8
Frontend/src/models/dtos/Reportes/ExistenciaPapelDto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface ExistenciaPapelDto {
|
||||
tipoBobina: string;
|
||||
bobinasEnStock: number | null;
|
||||
totalKilosEnStock: number | null;
|
||||
consumoAcumulado: number | null;
|
||||
promedioDiasDisponibles: number | null;
|
||||
fechaEstimacionFinStock?: string | null;
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
// Define las sub-pestañas del módulo Contables
|
||||
const contablesSubModules = [
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' }, // Se convertirá en /contables/tipos-pago
|
||||
const contablesSubModules = [
|
||||
{ label: 'Pagos Distribuidores', path: 'pagos-distribuidores' },
|
||||
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
||||
];
|
||||
|
||||
const ContablesIndexPage: React.FC = () => {
|
||||
|
||||
@@ -36,8 +36,8 @@ const GestionarNotasCDPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroDestino, setFiltroDestino] = useState<DestinoFiltroType>('');
|
||||
const [filtroIdDestinatario, setFiltroIdDestinatario] = useState<number | string>('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
@@ -31,8 +31,8 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);//useState('');
|
||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>('');
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const CtrlDevolucionesPage: React.FC = () => {
|
||||
return <Typography variant="h6">Página de Gestión del Control de Devoluciones</Typography>;
|
||||
};
|
||||
export default CtrlDevolucionesPage;
|
||||
@@ -150,7 +150,7 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Solo Activos"
|
||||
label="Ver Activos"
|
||||
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
|
||||
@@ -29,8 +29,8 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
|
||||
@@ -36,8 +36,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
|
||||
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
|
||||
|
||||
@@ -32,8 +32,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
|
||||
|
||||
@@ -30,8 +30,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
||||
|
||||
|
||||
260
Frontend/src/pages/Reportes/ReporteExistenciaPapelPage.tsx
Normal file
260
Frontend/src/pages/Reportes/ReporteExistenciaPapelPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
// src/pages/Reportes/ReporteExistenciaPapelPage.tsx
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from '@mui/material';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ExistenciaPapelDto } from '../../models/dtos/Reportes/ExistenciaPapelDto';
|
||||
import SeleccionaReporteExistenciaPapel from './SeleccionaReporteExistenciaPapel';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
const ReporteExistenciaPapelPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ExistenciaPapelDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
idPlanta?: number | null;
|
||||
consolidado: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
idPlanta?: number | null;
|
||||
consolidado: boolean;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(params);
|
||||
try {
|
||||
const data = await reportesService.getExistenciaPapel(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
setReportData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
setReportData([]);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (reportData.length === 0) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Data inicial formateada
|
||||
const dataToExport: Record<string, any>[] = reportData.map(item => {
|
||||
let fechaString = '-';
|
||||
if (item.fechaEstimacionFinStock) {
|
||||
const d = new Date(item.fechaEstimacionFinStock);
|
||||
if (!isNaN(d.getTime())) {
|
||||
fechaString = d.toLocaleDateString('es-AR', { timeZone: 'UTC' });
|
||||
}
|
||||
}
|
||||
return {
|
||||
"Tipo Bobina": item.tipoBobina,
|
||||
"Bobinas Stock": item.bobinasEnStock ?? 0,
|
||||
"Kg Stock": item.totalKilosEnStock ?? 0,
|
||||
"Consumo Acum. (Kg)": item.consumoAcumulado ?? 0,
|
||||
"Días Disp. (Prom.)": item.promedioDiasDisponibles != null
|
||||
? Math.round(item.promedioDiasDisponibles)
|
||||
: '-',
|
||||
"Fecha Est. Fin Stock": fechaString,
|
||||
};
|
||||
});
|
||||
|
||||
// 2) Cálculo de totales
|
||||
const totales = dataToExport.reduce(
|
||||
(acc, row) => {
|
||||
acc.bobinas += Number(row["Bobinas Stock"]);
|
||||
acc.kilos += Number(row["Kg Stock"]);
|
||||
acc.consumo += Number(row["Consumo Acum. (Kg)"]);
|
||||
return acc;
|
||||
},
|
||||
{ bobinas: 0, kilos: 0, consumo: 0 }
|
||||
);
|
||||
|
||||
// 3) Insertamos la fila de totales
|
||||
dataToExport.push({
|
||||
"Tipo Bobina": "Totales",
|
||||
"Bobinas Stock": totales.bobinas,
|
||||
"Kg Stock": totales.kilos,
|
||||
"Consumo Acum. (Kg)": totales.consumo,
|
||||
"Días Disp. (Prom.)": '-', // o lo que prefieras
|
||||
"Fecha Est. Fin Stock": '-' // vacío o guión
|
||||
});
|
||||
|
||||
// 4) Creamos la hoja
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
|
||||
// 5) Auto‐anchos
|
||||
const headers = Object.keys(dataToExport[0]);
|
||||
ws['!cols'] = headers.map(h => {
|
||||
const maxLen = dataToExport.reduce((prev, row) => {
|
||||
const cell = row[h]?.toString() ?? '';
|
||||
return Math.max(prev, cell.length);
|
||||
}, h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
|
||||
// 6) Congelamos la primera fila
|
||||
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
|
||||
// 7) Libro y guardado
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ExistenciaPapel");
|
||||
|
||||
let fileName = "ReporteExistenciaPapel";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
if (currentParams.consolidado) fileName += "_Consolidado";
|
||||
else if (currentParams.idPlanta) fileName += `_Planta${currentParams.idPlanta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [reportData, currentParams]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
if (!currentParams) {
|
||||
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
|
||||
return;
|
||||
}
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getExistenciaPapelPdf(currentParams);
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
if (!w) alert("Permite popups para ver el PDF.");
|
||||
}
|
||||
} catch {
|
||||
setError('Ocurrió un error al generar el PDF.');
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteExistenciaPapel
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
onCancel={handleVolverAParametros}
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Existencia de Papel</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
onClick={handleGenerarYAbrirPdf}
|
||||
variant="contained"
|
||||
disabled={loadingPdf || reportData.length === 0 || !!error}
|
||||
size="small"
|
||||
>
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExportToExcel}
|
||||
variant="outlined"
|
||||
disabled={reportData.length === 0 || !!error}
|
||||
size="small"
|
||||
>
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Nuevos Parámetros
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Bobina</TableCell>
|
||||
<TableCell align="right">Cant. Stock</TableCell>
|
||||
<TableCell align="right">Kg. Stock</TableCell>
|
||||
<TableCell align="right">Consumo Acum. (Kg)</TableCell>
|
||||
<TableCell align="right">Días Disp. (Prom.)</TableCell>
|
||||
<TableCell>Fecha Est. Fin Stock</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.map((row, idx) => {
|
||||
const d = row.fechaEstimacionFinStock ? new Date(row.fechaEstimacionFinStock) : null;
|
||||
const fechaFmt = d && !isNaN(d.getTime())
|
||||
? d.toLocaleDateString('es-AR', { timeZone: 'UTC' })
|
||||
: '-';
|
||||
return (
|
||||
<TableRow key={row.tipoBobina + idx}>
|
||||
<TableCell>{row.tipoBobina}</TableCell>
|
||||
<TableCell align="right">{row.bobinasEnStock?.toLocaleString('es-AR') ?? '-'}</TableCell>
|
||||
<TableCell align="right">{row.totalKilosEnStock?.toLocaleString('es-AR') ?? '-'}</TableCell>
|
||||
<TableCell align="right">{row.consumoAcumulado?.toLocaleString('es-AR') ?? '-'}</TableCell>
|
||||
<TableCell align="right">{row.promedioDiasDisponibles != null ? Math.round(row.promedioDiasDisponibles).toLocaleString('es-AR') : '-'}</TableCell>
|
||||
<TableCell>{fechaFmt}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteExistenciaPapelPage;
|
||||
91
Frontend/src/pages/Reportes/ReportesIndexPage.tsx
Normal file
91
Frontend/src/pages/Reportes/ReportesIndexPage.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const reportesSubModules = [
|
||||
{ label: 'Existencia de Papel', path: 'existencia-papel' },
|
||||
// { label: 'Consumo Bobinas Mensual', path: 'consumo-bobinas-mensual' }, // Ejemplo
|
||||
// ... agregar otros reportes aquí a medida que se implementen
|
||||
];
|
||||
|
||||
const ReportesIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/reportes';
|
||||
// Extrae la parte de la ruta que sigue a '/reportes/'
|
||||
const subPathSegment = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Toma solo el primer segmento
|
||||
: undefined;
|
||||
|
||||
let activeTabIndex = -1;
|
||||
|
||||
if (subPathSegment) {
|
||||
activeTabIndex = reportesSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPathSegment
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
// Si estamos exactamente en '/reportes' y hay sub-módulos, navegar al primero.
|
||||
if (location.pathname === currentBasePath && reportesSubModules.length > 0) {
|
||||
navigate(reportesSubModules[0].path, { replace: true }); // Navega a la sub-ruta
|
||||
// setSelectedSubTab(0); // Esto se manejará en la siguiente ejecución del useEffect debido al cambio de ruta
|
||||
} else {
|
||||
setSelectedSubTab(false); // Ninguna sub-ruta activa o conocida, o no hay sub-módulos
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]); // Solo depende de location.pathname y navigate
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
// No es necesario setSelectedSubTab aquí directamente, el useEffect lo manejará.
|
||||
navigate(reportesSubModules[newValue].path);
|
||||
};
|
||||
|
||||
// Si no hay sub-módulos definidos, podría ser un estado inicial
|
||||
if (reportesSubModules.length === 0) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h5" gutterBottom>Módulo de Reportes</Typography>
|
||||
<Typography>No hay reportes configurados.</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Módulo de Reportes
|
||||
</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs
|
||||
value={selectedSubTab} // 'false' es un valor válido para Tabs si ninguna pestaña está seleccionada
|
||||
onChange={handleSubTabChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos de reportes"
|
||||
>
|
||||
{reportesSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
{/* Outlet renderizará ReporteExistenciaPapelPage u otros
|
||||
Solo renderiza el Outlet si hay una pestaña seleccionada VÁLIDA.
|
||||
Si selectedSubTab es 'false' (porque ninguna ruta coincide con los sub-módulos),
|
||||
se muestra el mensaje.
|
||||
*/}
|
||||
{selectedSubTab !== false ? <Outlet /> : <Typography sx={{p:2}}>Seleccione un reporte del menú lateral o de las pestañas.</Typography>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportesIndexPage;
|
||||
156
Frontend/src/pages/Reportes/SeleccionaReporteExistenciaPapel.tsx
Normal file
156
Frontend/src/pages/Reportes/SeleccionaReporteExistenciaPapel.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel
|
||||
} from '@mui/material';
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import plantaService from '../../services/Impresion/plantaService';
|
||||
|
||||
interface SeleccionaReporteExistenciaPapelProps {
|
||||
onGenerarReporte: (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
idPlanta?: number | null;
|
||||
consolidado: boolean;
|
||||
}) => Promise<void>; // La función que realmente llama al servicio y maneja los datos
|
||||
onCancel: () => void; // Para cerrar el modal/componente
|
||||
isLoading?: boolean; // Para mostrar estado de carga desde el padre
|
||||
apiErrorMessage?: string | null; // Para mostrar errores de API desde el padre
|
||||
}
|
||||
|
||||
const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPapelProps> = ({
|
||||
onGenerarReporte,
|
||||
onCancel,
|
||||
isLoading,
|
||||
apiErrorMessage
|
||||
}) => {
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [idPlanta, setIdPlanta] = useState<number | string>('');
|
||||
const [consolidado, setConsolidado] = useState<boolean>(false);
|
||||
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlantas = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const plantasData = await plantaService.getAllPlantas();
|
||||
setPlantas(plantasData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar plantas:", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar plantas.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
fetchPlantas();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Si se marca consolidado, limpiar y deshabilitar la selección de planta
|
||||
if (consolidado) {
|
||||
setIdPlanta('');
|
||||
}
|
||||
}, [consolidado]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.';
|
||||
if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.';
|
||||
if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) {
|
||||
errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.';
|
||||
}
|
||||
if (!consolidado && !idPlanta) {
|
||||
errors.idPlanta = 'Seleccione una planta si no es consolidado.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleGenerar = () => {
|
||||
if (!validate()) return;
|
||||
onGenerarReporte({
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
idPlanta: consolidado ? null : Number(idPlanta),
|
||||
consolidado
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Parámetros: Existencia de Papel
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Fecha Desde"
|
||||
type="date"
|
||||
value={fechaDesde}
|
||||
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaDesde}
|
||||
helperText={localErrors.fechaDesde}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Fecha Hasta"
|
||||
type="date"
|
||||
value={fechaHasta}
|
||||
onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaHasta}
|
||||
helperText={localErrors.fechaHasta}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={consolidado}
|
||||
onChange={(e) => setConsolidado(e.target.checked)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label="Consolidado (Todas las Plantas)"
|
||||
sx={{ mt: 1, mb: 1 }}
|
||||
/>
|
||||
<FormControl fullWidth margin="normal" error={!!localErrors.idPlanta} disabled={isLoading || loadingDropdowns || consolidado}>
|
||||
<InputLabel id="planta-select-label" required={!consolidado}>Planta</InputLabel>
|
||||
<Select
|
||||
labelId="planta-select-label"
|
||||
label="Planta"
|
||||
value={consolidado ? '' : idPlanta} // Limpiar selección si es consolidado
|
||||
onChange={(e) => { setIdPlanta(e.target.value as number); setLocalErrors(p => ({ ...p, idPlanta: null })); }}
|
||||
>
|
||||
<MenuItem value="" disabled><em>{consolidado ? 'N/A (Consolidado)' : 'Seleccione una planta'}</em></MenuItem>
|
||||
{plantas.map((p) => (
|
||||
<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.idPlanta && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPlanta}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onCancel} color="secondary" disabled={isLoading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
|
||||
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeleccionaReporteExistenciaPapel;
|
||||
@@ -20,8 +20,8 @@ const GestionarAuditoriaUsuariosPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroIdUsuarioAfectado, setFiltroIdUsuarioAfectado] = useState<UsuarioDto | null>(null);
|
||||
const [filtroIdUsuarioModifico, setFiltroIdUsuarioModifico] = useState<UsuarioDto | null>(null);
|
||||
const [filtroTipoMod, setFiltroTipoMod] = useState('');
|
||||
|
||||
@@ -52,6 +52,10 @@ import GestionarRitmosPage from '../pages/Radios/GestionarRitmosPage';
|
||||
import GestionarCancionesPage from '../pages/Radios/GestionarCancionesPage';
|
||||
import GenerarListasRadioPage from '../pages/Radios/GenerarListasRadioPage';
|
||||
|
||||
// Reportes
|
||||
import ReportesIndexPage from '../pages/Reportes/ReportesIndexPage'; // Crear este si no existe
|
||||
import ReporteExistenciaPapelPage from '../pages/Reportes/ReporteExistenciaPapelPage';
|
||||
|
||||
// Auditorias
|
||||
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
||||
|
||||
@@ -150,8 +154,12 @@ const AppRoutes = () => {
|
||||
<Route path="tiradas" element={<GestionarTiradasPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */}
|
||||
<Route path="reportes" element={<PlaceholderPage moduleName="Reportes" />} />
|
||||
{/* Módulo de Reportes */}
|
||||
<Route path="reportes" element={<ReportesIndexPage />}> {/* Página principal del módulo */}
|
||||
<Route index element={<Typography sx={{p:2}}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
|
||||
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
|
||||
{/* Aquí se añadirán las rutas para otros reportes */}
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Radios (anidado) */}
|
||||
<Route path="radios" element={<RadiosIndexPage />}>
|
||||
|
||||
52
Frontend/src/services/Reportes/reportesService.ts
Normal file
52
Frontend/src/services/Reportes/reportesService.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { ExistenciaPapelDto } from '../../models/dtos/Reportes/ExistenciaPapelDto';
|
||||
|
||||
interface GetExistenciaPapelParams {
|
||||
fechaDesde: string; // yyyy-MM-dd
|
||||
fechaHasta: string; // yyyy-MM-dd
|
||||
idPlanta?: number | null;
|
||||
consolidado: boolean;
|
||||
}
|
||||
|
||||
const getExistenciaPapelPdf = async (params: GetExistenciaPapelParams): Promise<Blob> => {
|
||||
const queryParams: Record<string, string | number | boolean> = {
|
||||
fechaDesde: params.fechaDesde,
|
||||
fechaHasta: params.fechaHasta,
|
||||
consolidado: params.consolidado,
|
||||
};
|
||||
if (params.idPlanta && !params.consolidado) {
|
||||
queryParams.idPlanta = params.idPlanta;
|
||||
}
|
||||
|
||||
const response = await apiClient.get('/reportes/existencia-papel/pdf', {
|
||||
params: queryParams,
|
||||
responseType: 'blob', // ¡Importante para descargar archivos!
|
||||
});
|
||||
return response.data; // response.data será un Blob
|
||||
};
|
||||
|
||||
const getExistenciaPapel = async (params: GetExistenciaPapelParams): Promise<ExistenciaPapelDto[]> => {
|
||||
// Construir los query params, omitiendo idPlanta si es consolidado o no está definido
|
||||
const queryParams: Record<string, string | number | boolean> = {
|
||||
fechaDesde: params.fechaDesde,
|
||||
fechaHasta: params.fechaHasta,
|
||||
consolidado: params.consolidado,
|
||||
};
|
||||
if (params.idPlanta && !params.consolidado) {
|
||||
queryParams.idPlanta = params.idPlanta;
|
||||
}
|
||||
|
||||
const response = await apiClient.get<ExistenciaPapelDto[]>('/reportes/existencia-papel', { params: queryParams });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
// ... Aquí irán los métodos para otros reportes ...
|
||||
|
||||
const reportesService = {
|
||||
getExistenciaPapel,
|
||||
getExistenciaPapelPdf,
|
||||
// ...
|
||||
};
|
||||
|
||||
export default reportesService;
|
||||
Reference in New Issue
Block a user