Continuación de CRUDs e inicio de Reportes.

This commit is contained in:
2025-05-27 11:21:00 -03:00
parent 3c1fe15b1f
commit 298bc0d094
61 changed files with 41554 additions and 33 deletions

View File

@@ -11,12 +11,14 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/material": "^7.1.0",
"@mui/x-data-grid": "^8.4.0",
"axios": "^1.9.0",
"jwt-decode": "^4.0.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.5.3"
"react-router-dom": "^7.5.3",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
@@ -1426,6 +1428,64 @@
}
}
},
"node_modules/@mui/x-data-grid": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.4.0.tgz",
"integrity": "sha512-c0fgMhvQTjCSo3LgRK1Mdk2msktCl9uwMYUYlP6bbqJ7I03IvS+1aZ+s3nSLmaq1aVh7sE2Bnuz63OnVerTLJA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.0.2",
"@mui/x-internals": "8.4.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.4.0.tgz",
"integrity": "sha512-Z7FCahC4MLfTVzEwnKOB7P1fiR9DzFuMzHOPRNaMXc/rsS7unbtBKAG94yvsRzReCyjzZUVA7h37lnQ1DoPKJw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2136,6 +2196,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2363,6 +2432,19 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2389,6 +2471,15 @@
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2516,6 +2607,18 @@
"node": ">= 6"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3239,6 +3342,15 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -4335,6 +4447,12 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -4670,6 +4788,18 @@
"node": ">=0.10.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -4927,6 +5057,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -5056,6 +5195,24 @@
"node": ">= 8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5073,6 +5230,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -13,12 +13,14 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/material": "^7.1.0",
"@mui/x-data-grid": "^8.4.0",
"axios": "^1.9.0",
"jwt-decode": "^4.0.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.5.3"
"react-router-dom": "^7.5.3",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/js": "^9.25.0",

View 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;
}

View File

@@ -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 = () => {

View File

@@ -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>('');

View File

@@ -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' | ''>('');

View File

@@ -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;

View File

@@ -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> */}

View File

@@ -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[]>([]);

View File

@@ -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');

View File

@@ -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' | ''>('');

View File

@@ -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>('');

View 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) Autoanchos
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;

View 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;

View 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;

View File

@@ -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('');

View File

@@ -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 />}>

View 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;