Comenzando la implementación final de permisos y depuración. Se sigue...

This commit is contained in:
2025-06-03 18:42:56 -03:00
parent 062cc05fd0
commit 8fb94f8cef
46 changed files with 711 additions and 493 deletions

View File

@@ -3,17 +3,18 @@ import {
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import type {Theme} from '@mui/material/styles';
import type { Theme } from '@mui/material/styles';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
import SeleccionaReporteComparativaConsumoBobinas from './SeleccionaReporteComparativaConsumoBobinas';
import { usePermissions } from '../../hooks/usePermissions';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Interfaz extendida para DataGrid
interface ComparativaConsumoBobinasDataGridDto extends ComparativaConsumoBobinasDto {
id: string;
id: string;
}
const ReporteComparativaConsumoBobinasPage: React.FC = () => {
@@ -28,9 +29,11 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
nombrePlanta?: string;
mesA?: string;
mesB?: string;
mesA?: string;
mesB?: string;
} | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVerReporte = isSuperAdmin || tienePermiso("RR007");
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
@@ -40,6 +43,11 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
}) => {
if (!puedeVerReporte) {
setError("No tiene permiso para generar este reporte.");
setLoading(false);
return;
}
setLoading(true);
setError(null);
setApiErrorParams(null);
@@ -47,23 +55,23 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
let plantaNombre = "Consolidado";
if (!params.consolidado && params.idPlanta) {
const plantaService = (await import('../../services/Impresion/plantaService')).default;
const plantaData = await plantaService.getPlantaById(params.idPlanta);
plantaNombre = plantaData?.nombre ?? "N/A";
const plantaService = (await import('../../services/Impresion/plantaService')).default;
const plantaData = await plantaService.getPlantaById(params.idPlanta);
plantaNombre = plantaData?.nombre ?? "N/A";
}
// Formatear nombres de meses para el PDF
const formatMonthYear = (dateString: string) => {
const date = new Date(dateString + 'T00:00:00'); // Asegurar que se parsea como local
return date.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
const date = new Date(dateString + 'T00:00:00'); // Asegurar que se parsea como local
return date.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
};
setCurrentParams({
...params,
nombrePlanta: plantaNombre,
mesA: formatMonthYear(params.fechaInicioMesA),
mesB: formatMonthYear(params.fechaInicioMesB)
...params,
nombrePlanta: plantaNombre,
mesA: formatMonthYear(params.fechaInicioMesA),
mesB: formatMonthYear(params.fechaInicioMesB)
});
try {
const data = await reportesService.getComparativaConsumoBobinas(params);
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.tipoBobina}-${index}` }));
@@ -106,23 +114,23 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
}));
const totales = dataToExport.reduce((acc, row) => {
acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]);
acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]);
acc.kgB += Number(row["Kg Mes B"]); acc.difKg += Number(row["Dif. Kg"]);
return acc;
}, { cantA:0, cantB:0, difCant:0, kgA:0, kgB:0, difKg:0 });
acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]);
acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]);
acc.kgB += Number(row["Kg Mes B"]); acc.difKg += Number(row["Dif. Kg"]);
return acc;
}, { cantA: 0, cantB: 0, difCant: 0, kgA: 0, kgB: 0, difKg: 0 });
dataToExport.push({
"Tipo Bobina": "TOTALES", "Cant. Mes A": totales.cantA, "Cant. Mes B": totales.cantB,
"Dif. Cant.": totales.difCant, "Kg Mes A": totales.kgA, "Kg Mes B": totales.kgB,
"Dif. Kg": totales.difKg,
"Tipo Bobina": "TOTALES", "Cant. Mes A": totales.cantA, "Cant. Mes B": totales.cantB,
"Dif. Cant.": totales.difCant, "Kg Mes A": totales.kgA, "Kg Mes B": totales.kgB,
"Dif. Kg": totales.difKg,
});
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => {
const maxLen = Math.max(...dataToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
return { wch: maxLen + 2 };
ws['!cols'] = headers.map(h => {
const maxLen = Math.max(...dataToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
return { wch: maxLen + 2 };
});
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
const wb = XLSX.utils.book_new();
@@ -192,88 +200,91 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
if (!totalesGenerales) return null;
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
const colConfig = columns.find(c => c.field === field);
// Ajustar anchos para los totales para que sean más compactos
let targetWidth: number | string = 'auto';
let targetMinWidth: number | string = 'auto';
const colConfig = columns.find(c => c.field === field);
// Ajustar anchos para los totales para que sean más compactos
let targetWidth: number | string = 'auto';
let targetMinWidth: number | string = 'auto';
if (isLabel) {
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.5) : 100; // Más corto para "TOTALES:"
targetMinWidth = 80;
} else if (colConfig) {
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.75) : 90; // 75% del ancho de columna, mínimo 70
targetMinWidth = 70;
}
const style: React.CSSProperties = {
minWidth: targetMinWidth,
width: targetWidth,
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
paddingRight: isLabel ? 1 : (field === 'diferenciaKilosUtilizados' ? 0 : 1), // pr en theme units
fontWeight: 'bold',
whiteSpace: 'nowrap',
};
if (isLabel) {
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.5) : 100; // Más corto para "TOTALES:"
targetMinWidth = 80;
} else if (colConfig) {
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.75) : 90; // 75% del ancho de columna, mínimo 70
targetMinWidth = 70;
}
// Aplicar el separador si es la columna 'kilosUtilizadosMesA'
if (field === 'kilosUtilizadosMesA') {
style.borderLeft = `2px solid grey`; // O theme.palette.divider
style.paddingLeft = '8px'; // Espacio después del separador
}
return style;
const style: React.CSSProperties = {
minWidth: targetMinWidth,
width: targetWidth,
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
paddingRight: isLabel ? 1 : (field === 'diferenciaKilosUtilizados' ? 0 : 1), // pr en theme units
fontWeight: 'bold',
whiteSpace: 'nowrap',
};
// Aplicar el separador si es la columna 'kilosUtilizadosMesA'
if (field === 'kilosUtilizadosMesA') {
style.borderLeft = `2px solid grey`; // O theme.palette.divider
style.paddingLeft = '8px'; // Espacio después del separador
}
return style;
};
return (
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
flexShrink: 0,
overflow: 'hidden',
px: 1,
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
flexShrink: 0,
overflow: 'hidden',
px:1,
}}>
<GridFooter
sx={{
borderTop: 'none',
width: 'auto',
'& .MuiToolbar-root': {
paddingLeft: 0,
paddingRight: 0,
},
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
<GridFooter
sx={{
borderTop: 'none',
width: 'auto',
'& .MuiToolbar-root': {
paddingLeft: 0,
paddingRight: 0,
},
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflowX: 'auto',
px:1,
flexShrink: 1,
}}>
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesA')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesA)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesB')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesB)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('diferenciaBobinasUtilizadas')}>{numberLocaleFormatter(totalesGenerales.diferenciaBobinasUtilizadas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesA')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesA)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesB')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesB)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('diferenciaKilosUtilizados')}>{numberLocaleFormatter(totalesGenerales.diferenciaKilosUtilizados)}</Typography>
</Box>
</GridFooterContainer>
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflowX: 'auto',
px: 1,
flexShrink: 1,
}}>
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesA')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesA)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesB')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesB)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('diferenciaBobinasUtilizadas')}>{numberLocaleFormatter(totalesGenerales.diferenciaBobinasUtilizadas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesA')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesA)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesB')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesB)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('diferenciaKilosUtilizados')}>{numberLocaleFormatter(totalesGenerales.diferenciaKilosUtilizados)}</Typography>
</Box>
</GridFooterContainer>
);
};
if (showParamSelector) {
if (!loading && !puedeVerReporte) {
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
}
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
@@ -293,7 +304,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Comparativa Consumo de Bobinas</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
<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">
@@ -305,16 +316,16 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && (
<Paper sx={{
width: '100%',
mt: 2,
'& .separator-left': {
borderLeft: (theme: Theme) => `2px solid ${theme.palette.divider}`,
},
<Paper sx={{
width: '100%',
mt: 2,
'& .separator-left': {
borderLeft: (theme: Theme) => `2px solid ${theme.palette.divider}`,
},
}}>
<DataGrid
rows={rows}
@@ -325,16 +336,16 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
sx={{ height: 'calc(100vh - 300px)' }} // Ajusta esta altura según sea necesario
initialState={{
pagination: {
paginationModel: { pageSize: 10, page: 0 },
paginationModel: { pageSize: 10, page: 0 },
},
}}
pageSizeOptions={[5, 10, 25, 50]}
disableRowSelectionOnClick
// hideFooterSelectedRowCount // Ya se maneja en CustomFooter
// hideFooterSelectedRowCount // Ya se maneja en CustomFooter
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};