Continuidad de reportes Frontend. Se sigue..

This commit is contained in:
2025-05-28 18:58:45 -03:00
parent 2273ebb1e0
commit 70fc847721
23 changed files with 1645 additions and 11 deletions

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+cdd4d3e0f71f866aabb489394a273ab4d013284c")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2273ebb1e018273a6e35d3f9ab0afe55ae1814bc")]
[assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -0,0 +1,9 @@
export interface ListadoDistribucionCanillasPromedioDiaDto {
dia: string; // Nombre del día de la semana
cant: number;
llevados: number;
devueltos: number;
promedio_Llevados: number;
promedio_Devueltos: number;
promedio_Ventas: number;
}

View File

@@ -0,0 +1,7 @@
import type { ListadoDistribucionCanillasSimpleDto } from './ListadoDistribucionCanillasSimpleDto';
import type { ListadoDistribucionCanillasPromedioDiaDto } from './ListadoDistribucionCanillasPromedioDiaDto';
export interface ListadoDistribucionCanillasResponseDto {
detalleSimple: ListadoDistribucionCanillasSimpleDto[];
promediosPorDia: ListadoDistribucionCanillasPromedioDiaDto[];
}

View File

@@ -0,0 +1,5 @@
export interface ListadoDistribucionCanillasSimpleDto {
dia: number; // Día del mes
llevados: number;
devueltos: number;
}

View File

@@ -0,0 +1,10 @@
export interface ListadoDistribucionGeneralPromedioDiaDto {
dia: string;
cantidadDias: number;
promedioTirada: number;
promedioSinCargo: number;
promedioPerdidos: number;
promedioLlevados: number;
promedioDevueltos: number;
promedioVendidos: number;
}

View File

@@ -0,0 +1,7 @@
import type { ListadoDistribucionGeneralResumenDto } from './ListadoDistribucionGeneralResumenDto';
import type { ListadoDistribucionGeneralPromedioDiaDto } from './ListadoDistribucionGeneralPromedioDiaDto';
export interface ListadoDistribucionGeneralResponseDto {
resumen: ListadoDistribucionGeneralResumenDto[];
promediosPorDia: ListadoDistribucionGeneralPromedioDiaDto[];
}

View File

@@ -0,0 +1,9 @@
export interface ListadoDistribucionGeneralResumenDto {
fecha: string; // o Date, si prefieres parsear en el frontend
cantidadTirada: number;
sinCargo: number;
perdidos: number;
llevados: number;
devueltos: number;
vendidos: number;
}

View File

@@ -0,0 +1,7 @@
export interface MovimientoBobinaEstadoDetalleDto {
tipoBobina: string;
numeroRemito: string;
fechaMovimiento: string; // o Date, pero string es más simple para la tabla si ya viene formateado
cantidad: number;
tipoMovimiento: string; // "Ingreso", "Utilizada", "Dañada"
}

View File

@@ -0,0 +1,5 @@
export interface MovimientoBobinaEstadoTotalDto {
tipoMovimiento: string; // "Ingresos", "Utilizadas", "Dañadas"
totalBobinas: number;
totalKilos: number;
}

View File

@@ -0,0 +1,13 @@
export interface MovimientoBobinasDto {
tipoBobina: string;
bobinasIniciales: number;
kilosIniciales: number;
bobinasCompradas: number;
kilosComprados: number;
bobinasConsumidas: number;
kilosConsumidos: number;
bobinasDaniadas: number;
kilosDaniados: number;
bobinasFinales: number;
kilosFinales: number;
}

View File

@@ -0,0 +1,7 @@
import type { MovimientoBobinaEstadoDetalleDto } from "./MovimientoBobinaEstadoDetalleDto";
import type { MovimientoBobinaEstadoTotalDto } from "./MovimientoBobinaEstadoTotalDto";
export interface MovimientoBobinasPorEstadoResponseDto {
detalle: MovimientoBobinaEstadoDetalleDto[];
totales: MovimientoBobinaEstadoTotalDto[];
}

View File

@@ -0,0 +1,225 @@
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 { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto';
import SeleccionaReporteListadoDistribucionCanillas from './SeleccionaReporteListadoDistribucionCanillas';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteListadoDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionCanillasResponseDto | null>(null);
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<{
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
nombrePublicacion?: string;
} | null>(null);
const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
const pubData = await pubService.getPublicacionById(params.idPublicacion);
setCurrentParams({...params, nombrePublicacion: pubData?.nombre });
try {
const data = await reportesService.getListadoDistribucionCanillas(params);
setReportData(data);
if ((!data.detalleSimple || data.detalleSimple.length === 0) && (!data.promediosPorDia || data.promediosPorDia.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(null);
} finally {
setLoading(false);
}
}, []);
const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true);
setReportData(null);
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.detalleSimple?.length && !reportData.promediosPorDia?.length)) {
alert("No hay datos para exportar.");
return;
}
const wb = XLSX.utils.book_new();
if (reportData.detalleSimple?.length) {
const simpleToExport = reportData.detalleSimple.map(item => ({
"Día": item.dia,
"Llevados": item.llevados,
"Devueltos": item.devueltos,
"Vendidos": item.llevados - item.devueltos,
}));
const wsSimple = XLSX.utils.json_to_sheet(simpleToExport);
XLSX.utils.book_append_sheet(wb, wsSimple, "DetalleDiario");
}
if (reportData.promediosPorDia?.length) {
const promediosToExport = reportData.promediosPorDia.map(item => ({
"Día Semana": item.dia,
"Cant. Días": item.cant,
"Prom. Llevados": item.promedio_Llevados,
"Prom. Devueltos": item.promedio_Devueltos,
"Prom. Vendidos": item.promedio_Ventas,
}));
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDiaSemana");
}
let fileName = "ListadoDistribucionCanillas";
if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
}
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.getListadoDistribucionCanillasPdf(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}>
<SeleccionaReporteListadoDistribucionCanillas
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: Listado Distribución Canillitas</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!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 && reportData && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Detalle Diario</Typography>
{reportData.detalleSimple && reportData.detalleSimple.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Día</TableCell>
<TableCell align="right">Llevados</TableCell>
<TableCell align="right">Devueltos</TableCell>
<TableCell align="right">Vendidos</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.detalleSimple.map((row, idx) => (
<TableRow key={`simple-${idx}`}>
<TableCell>{row.dia}</TableCell>
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{(row.llevados - row.devueltos).toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (<Typography>No hay datos de detalle diario.</Typography>)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
{reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '300px' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Día Semana</TableCell>
<TableCell align="right">Cant. Días</TableCell>
<TableCell align="right">Prom. Llevados</TableCell>
<TableCell align="right">Prom. Devueltos</TableCell>
<TableCell align="right">Prom. Ventas</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.promediosPorDia.map((row, idx) => (
<TableRow key={`promedio-${idx}`}>
<TableCell>{row.dia}</TableCell>
<TableCell align="right">{row.cant.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedio_Llevados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedio_Devueltos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedio_Ventas.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (<Typography>No hay datos de promedios por día.</Typography>)}
</>
)}
</Box>
);
};
export default ReporteListadoDistribucionCanillasPage;

View File

@@ -0,0 +1,252 @@
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 { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
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<{
idPublicacion: number;
fechaDesde: string; // Primer día del mes
fechaHasta: string; // Último día del mes
nombrePublicacion?: string; // Para el nombre del archivo
mesAnioParaNombreArchivo?: string; // Para el nombre del archivo (ej. YYYY-MM)
} | null>(null);
const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
// Para el nombre del archivo y título del PDF
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
const pubData = await pubService.getPublicacionById(params.idPublicacion);
const mesAnioParts = params.fechaDesde.split('-'); // YYYY-MM-DD -> [YYYY, MM, DD]
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
try {
const data = await reportesService.getListadoDistribucionGeneral(params);
setReportData(data);
if ((!data.resumen || data.resumen.length === 0) && (!data.promediosPorDia || data.promediosPorDia.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(null);
} finally {
setLoading(false);
}
}, []);
const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true);
setReportData(null);
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) {
alert("No hay datos para exportar.");
return;
}
const wb = XLSX.utils.book_new();
if (reportData.resumen?.length) {
const resumenToExport = reportData.resumen.map(item => ({
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
"Tirada": item.cantidadTirada,
"Sin Cargo": item.sinCargo,
"Perdidos": item.perdidos,
"Llevados": item.llevados,
"Devueltos": item.devueltos,
"Vendidos": item.vendidos,
}));
const wsResumen = XLSX.utils.json_to_sheet(resumenToExport);
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario");
}
if (reportData.promediosPorDia?.length) {
const promediosToExport = reportData.promediosPorDia.map(item => ({
"Día Semana": item.dia,
"Cant. Días": item.cantidadDias,
"Prom. Tirada": item.promedioTirada,
"Prom. Sin Cargo": item.promedioSinCargo,
"Prom. Perdidos": item.promedioPerdidos,
"Prom. Llevados": item.promedioLlevados,
"Prom. Devueltos": item.promedioDevueltos,
"Prom. Vendidos": item.promedioVendidos,
}));
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
}
let fileName = "ListadoDistribucionGeneral";
if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
}
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.getListadoDistribucionGeneralPdf({
idPublicacion: currentParams.idPublicacion,
fechaDesde: currentParams.fechaDesde, // El servicio y SP esperan fechaDesde para el mes/año
fechaHasta: currentParams.fechaHasta // El SP no usa esta, pero el servicio de reporte sí para el nombre
});
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}>
<SeleccionaReporteListadoDistribucionGeneral
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: Listado Distribución General</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!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 && reportData && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography>
{reportData.resumen && reportData.resumen.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Fecha</TableCell>
<TableCell align="right">Tirada</TableCell>
<TableCell align="right">Sin Cargo</TableCell>
<TableCell align="right">Perdidos</TableCell>
<TableCell align="right">Llevados</TableCell>
<TableCell align="right">Devueltos</TableCell>
<TableCell align="right">Vendidos</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.resumen.map((row, idx) => (
<TableRow key={`resumen-${idx}`}>
<TableCell>{row.fecha ? new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
<TableCell align="right">{row.cantidadTirada.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.sinCargo.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.perdidos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.vendidos.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (<Typography>No hay datos de resumen diario.</Typography>)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
{reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '300px' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Día</TableCell>
<TableCell align="right">Cant. Días</TableCell>
<TableCell align="right">Prom. Tirada</TableCell>
<TableCell align="right">Prom. Sin Cargo</TableCell>
<TableCell align="right">Prom. Perdidos</TableCell>
<TableCell align="right">Prom. Llevados</TableCell>
<TableCell align="right">Prom. Devueltos</TableCell>
<TableCell align="right">Prom. Vendidos</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.promediosPorDia.map((row, idx) => (
<TableRow key={`promedio-${idx}`}>
<TableCell>{row.dia}</TableCell>
<TableCell align="right">{row.cantidadDias.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioTirada.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioSinCargo.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioPerdidos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioLlevados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioDevueltos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioVendidos.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (<Typography>No hay datos de promedios por día.</Typography>)}
</>
)}
</Box>
);
};
export default ReporteListadoDistribucionGeneralPage;

View File

@@ -0,0 +1,257 @@
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 { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
import SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null);
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>(null);
const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setCurrentParams(params);
try {
const data = await reportesService.getMovimientoBobinasEstado(params);
setReportData(data);
if ((!data.detalle || data.detalle.length === 0) && (!data.totales || data.totales.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(null);
} finally {
setLoading(false);
}
}, []);
const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true);
setReportData(null);
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.detalle?.length && !reportData.totales?.length)) {
alert("No hay datos para exportar.");
return;
}
const wb = XLSX.utils.book_new();
// Hoja de Detalles
if (reportData.detalle?.length) {
const detalleToExport = reportData.detalle.map(item => ({
"Tipo Bobina": item.tipoBobina,
"Nro Remito": item.numeroRemito,
"Fecha Movimiento": item.fechaMovimiento ? new Date(item.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
"Cantidad": item.cantidad,
"Tipo Movimiento": item.tipoMovimiento,
}));
const wsDetalle = XLSX.utils.json_to_sheet(detalleToExport);
const headersDetalle = Object.keys(detalleToExport[0] || {});
wsDetalle['!cols'] = headersDetalle.map(h => {
const maxLen = detalleToExport.reduce((prev, row) => {
const cell = (row as any)[h]?.toString() ?? '';
return Math.max(prev, cell.length);
}, h.length);
return { wch: maxLen + 2 };
});
wsDetalle['!freeze'] = { xSplit: 0, ySplit: 1 };
XLSX.utils.book_append_sheet(wb, wsDetalle, "DetalleMovimientos");
}
// Hoja de Totales
if (reportData.totales?.length) {
const totalesToExport = reportData.totales.map(item => ({
"Tipo Movimiento": item.tipoMovimiento,
"Total Bobinas": item.totalBobinas,
"Total Kilos": item.totalKilos,
}));
const wsTotales = XLSX.utils.json_to_sheet(totalesToExport);
const headersTotales = Object.keys(totalesToExport[0] || {});
wsTotales['!cols'] = headersTotales.map(h => {
const maxLen = totalesToExport.reduce((prev, row) => {
const cell = (row as any)[h]?.toString() ?? '';
return Math.max(prev, cell.length);
}, h.length);
return { wch: maxLen + 2 };
});
wsTotales['!freeze'] = { xSplit: 0, ySplit: 1 };
XLSX.utils.book_append_sheet(wb, wsTotales, "TotalesPorEstado");
}
let fileName = "ReporteMovimientoBobinasEstado";
if (currentParams) {
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_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.getMovimientoBobinasEstadoPdf(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}>
<SeleccionaReporteMovimientoBobinasEstado
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: Movimiento de Bobinas por Estado</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
onClick={handleGenerarYAbrirPdf}
variant="contained"
disabled={loadingPdf || !reportData || (!reportData.detalle?.length && !reportData.totales?.length) || !!error}
size="small"
>
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button>
<Button
onClick={handleExportToExcel}
variant="outlined"
disabled={!reportData || (!reportData.detalle?.length && !reportData.totales?.length) || !!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 && reportData && (
<> {/* Usamos un Fragmento React para agrupar los elementos sin añadir un div extra */}
{/* Tabla de Detalle de Movimientos */}
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
Detalle de Movimientos
</Typography>
{reportData.detalle && reportData.detalle.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '400px', mb: 3 }}> {/* Añadido mb: 3 para espaciado */}
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Tipo Bobina</TableCell>
<TableCell>Nro Remito</TableCell>
<TableCell>Fecha Movimiento</TableCell>
<TableCell align="right">Cantidad</TableCell>
<TableCell>Tipo Movimiento</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.detalle.map((row, idx) => (
<TableRow key={`detalle-${idx}`}>
<TableCell>{row.tipoBobina}</TableCell>
<TableCell>{row.numeroRemito}</TableCell>
<TableCell>{row.fechaMovimiento ? new Date(row.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
<TableCell align="right">{row.cantidad.toLocaleString('es-AR')}</TableCell>
<TableCell>{row.tipoMovimiento}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Typography sx={{ mb: 3 }}>No hay detalles de movimientos para mostrar.</Typography>
)}
{/* Tabla de Totales por Estado */}
<Typography variant="h6" gutterBottom>
Totales por Estado
</Typography>
{reportData.totales && reportData.totales.length > 0 ? (
<TableContainer component={Paper} sx={{ maxWidth: '600px' }}> {/* Limitamos el ancho para tablas pequeñas */}
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Tipo Movimiento</TableCell>
<TableCell align="right">Total Bobinas</TableCell>
<TableCell align="right">Total Kilos</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.totales.map((row, idx) => (
<TableRow key={`total-${idx}`}>
<TableCell>{row.tipoMovimiento}</TableCell>
<TableCell align="right">{row.totalBobinas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.totalKilos.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Typography>No hay totales para mostrar.</Typography>
)}
</>
)}
</Box>
);
};
export default ReporteMovimientoBobinasEstadoPage;

View File

@@ -0,0 +1,212 @@
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 { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto';
import SeleccionaReporteMovimientoBobinas from './SeleccionaReporteMovimientoBobinas';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteMovimientoBobinasPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasDto[]>([]);
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>(null);
const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setCurrentParams(params);
try {
const data = await reportesService.getMovimientoBobinas(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;
}
const dataToExport = reportData.map(item => ({
"Tipo Bobina": item.tipoBobina,
"Bobinas Iniciales": item.bobinasIniciales,
"Kg Iniciales": item.kilosIniciales,
"Bobinas Compradas": item.bobinasCompradas,
"Kg Comprados": item.kilosComprados,
"Bobinas Consumidas": item.bobinasConsumidas,
"Kg Consumidos": item.kilosConsumidos,
"Bobinas Dañadas": item.bobinasDaniadas,
"Kg Dañados": item.kilosDaniados,
"Bobinas Finales": item.bobinasFinales,
"Kg Finales": item.kilosFinales,
}));
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0]);
ws['!cols'] = headers.map(h => {
const maxLen = dataToExport.reduce((prev, row) => {
const cell = (row as any)[h]?.toString() ?? '';
return Math.max(prev, cell.length);
}, h.length);
return { wch: maxLen + 2 };
});
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "MovimientoBobinas");
let fileName = "ReporteMovimientoBobinas";
if (currentParams) {
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_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.getMovimientoBobinasPdf(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}>
<SeleccionaReporteMovimientoBobinas
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: Movimiento de Bobinas</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. Ini.</TableCell>
<TableCell align="right">Kg Ini.</TableCell>
<TableCell align="right">Compradas</TableCell>
<TableCell align="right">Kg Compr.</TableCell>
<TableCell align="right">Consum.</TableCell>
<TableCell align="right">Kg Consum.</TableCell>
<TableCell align="right">Dañadas</TableCell>
<TableCell align="right">Kg Dañ.</TableCell>
<TableCell align="right">Cant. Fin.</TableCell>
<TableCell align="right">Kg Finales</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.map((row, idx) => (
<TableRow key={row.tipoBobina + idx}>
<TableCell>{row.tipoBobina}</TableCell>
<TableCell align="right">{row.bobinasIniciales.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosIniciales.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasCompradas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosComprados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasConsumidas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosConsumidos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasDaniadas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosDaniados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasFinales.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosFinales.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
);
};
export default ReporteMovimientoBobinasPage;

View File

@@ -4,8 +4,10 @@ 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
{ label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
{ label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
{ label: 'Distribución General', path: 'listado-distribucion-general' },
{ label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
];
const ReportesIndexPage: React.FC = () => {

View File

@@ -20,7 +20,6 @@ interface SeleccionaReporteExistenciaPapelProps {
const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPapelProps> = ({
onGenerarReporte,
onCancel,
isLoading,
apiErrorMessage
}) => {
@@ -142,9 +141,6 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape
{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>

View File

@@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import publicacionService from '../../services/Distribucion/publicacionService';
interface SeleccionaReporteListadoDistribucionCanillasProps {
onGenerarReporte: (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}) => Promise<void>;
onCancel: () => void;
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteListadoDistribucionCanillasProps> = ({
onGenerarReporte,
isLoading,
apiErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => {
const fetchPublicaciones = async () => {
setLoadingDropdowns(true);
try {
const data = await publicacionService.getAllPublicaciones(undefined, undefined, true);
setPublicaciones(data.map(p => p));
} catch (error) {
console.error("Error al cargar publicaciones:", error);
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' }));
} finally {
setLoadingDropdowns(false);
}
};
fetchPublicaciones();
}, []);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idPublicacion) errors.idPublicacion = 'Debe seleccionar una publicación.';
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.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleGenerar = () => {
if (!validate()) return;
onGenerarReporte({
idPublicacion: Number(idPublicacion),
fechaDesde,
fechaHasta
});
};
return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
<Typography variant="h6" gutterBottom>
Parámetros: Listado Distribución Canillitas
</Typography>
<FormControl fullWidth margin="normal" error={!!localErrors.idPublicacion} disabled={isLoading || loadingDropdowns}>
<InputLabel id="publicacion-select-label-can" required>Publicación</InputLabel>
<Select
labelId="publicacion-select-label-can"
label="Publicación"
value={idPublicacion}
onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({ ...p, idPublicacion: null })); }}
>
<MenuItem value="" disabled><em>Seleccione una publicación</em></MenuItem>
{publicaciones.map((p) => (
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>
))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>}
</FormControl>
<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 }}
/>
{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={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
</Button>
</Box>
</Box>
);
};
export default SeleccionaReporteListadoDistribucionCanillas;

View File

@@ -0,0 +1,120 @@
// src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx
import React, { useState, useEffect } from 'react';
import {
Box, Typography, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, TextField
} from '@mui/material';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import publicacionService from '../../services/Distribucion/publicacionService';
interface SeleccionaReporteListadoDistribucionGeneralProps {
onGenerarReporte: (params: {
idPublicacion: number;
fechaDesde: string; // Será el primer día del mes seleccionado
fechaHasta: string; // Será el último día del mes seleccionado
}) => Promise<void>;
onCancel: () => void;
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteListadoDistribucionGeneral: React.FC<SeleccionaReporteListadoDistribucionGeneralProps> = ({
onGenerarReporte,
isLoading,
apiErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
// Para el selector de mes/año, usamos un input type="month"
const [mesAnio, setMesAnio] = useState<string>(new Date().toISOString().substring(0, 7)); // Formato "YYYY-MM"
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => {
const fetchPublicaciones = async () => {
setLoadingDropdowns(true);
try {
// Asumiendo que quieres solo publicaciones habilitadas
const data = await publicacionService.getAllPublicaciones(undefined, undefined, true);
setPublicaciones(data.map(p => p)); // El servicio devuelve tupla
} catch (error) {
console.error("Error al cargar publicaciones:", error);
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' }));
} finally {
setLoadingDropdowns(false);
}
};
fetchPublicaciones();
}, []);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idPublicacion) errors.idPublicacion = 'Debe seleccionar una publicación.';
if (!mesAnio) errors.mesAnio = 'Debe seleccionar un Mes/Año.';
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleGenerar = () => {
if (!validate()) return;
const [year, month] = mesAnio.split('-').map(Number);
const fechaDesde = new Date(year, month - 1, 1).toISOString().split('T')[0];
const fechaHasta = new Date(year, month, 0).toISOString().split('T')[0]; // Último día del mes
onGenerarReporte({
idPublicacion: Number(idPublicacion),
fechaDesde,
fechaHasta
});
};
return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
<Typography variant="h6" gutterBottom>
Parámetros: Listado Distribución General
</Typography>
<FormControl fullWidth margin="normal" error={!!localErrors.idPublicacion} disabled={isLoading || loadingDropdowns}>
<InputLabel id="publicacion-select-label" required>Publicación</InputLabel>
<Select
labelId="publicacion-select-label"
label="Publicación"
value={idPublicacion}
onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({ ...p, idPublicacion: null })); }}
>
<MenuItem value="" disabled><em>Seleccione una publicación</em></MenuItem>
{publicaciones.map((p) => (
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>
))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>}
</FormControl>
<TextField
label="Mes y Año"
type="month" // Esto generará un selector de mes/año nativo del navegador
value={mesAnio}
onChange={(e) => { setMesAnio(e.target.value); setLocalErrors(p => ({ ...p, mesAnio: null })); }}
margin="normal"
fullWidth
required
error={!!localErrors.mesAnio}
helperText={localErrors.mesAnio}
disabled={isLoading}
InputLabelProps={{ shrink: true }}
/>
{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={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
</Button>
</Box>
</Box>
);
};
export default SeleccionaReporteListadoDistribucionGeneral;

View File

@@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; // Asumo que ya tienes este DTO
import plantaService from '../../services/Impresion/plantaService'; // Asumo que ya tienes este servicio
interface SeleccionaReporteMovimientoBobinasProps {
onGenerarReporte: (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number; // idPlanta es obligatoria aquí
}) => Promise<void>;
onCancel: () => void;
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteMovimientoBobinas: React.FC<SeleccionaReporteMovimientoBobinasProps> = ({
onGenerarReporte,
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>(''); // Puede ser string inicialmente por el MenuItem vacío
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(); // Asumiendo que esto devuelve todas
setPlantas(plantasData);
} catch (error) {
console.error("Error al cargar plantas:", error);
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar plantas.' }));
} finally {
setLoadingDropdowns(false);
}
};
fetchPlantas();
}, []);
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 (!idPlanta) {
errors.idPlanta = 'Debe seleccionar una planta.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleGenerar = () => {
if (!validate()) return;
onGenerarReporte({
fechaDesde,
fechaHasta,
idPlanta: Number(idPlanta) // Asegurarse de que es un número
});
};
return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
<Typography variant="h6" gutterBottom>
Parámetros: Movimiento de Bobinas
</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 }}
/>
<FormControl fullWidth margin="normal" error={!!localErrors.idPlanta} disabled={isLoading || loadingDropdowns}>
<InputLabel id="planta-select-label" required>Planta</InputLabel>
<Select
labelId="planta-select-label"
label="Planta"
value={idPlanta}
onChange={(e) => { setIdPlanta(e.target.value as number); setLocalErrors(p => ({ ...p, idPlanta: null })); }}
>
<MenuItem value="" disabled><em>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={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
</Button>
</Box>
</Box>
);
};
export default SeleccionaReporteMovimientoBobinas;

View File

@@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import plantaService from '../../services/Impresion/plantaService';
interface SeleccionaReporteMovimientoBobinasEstadoProps {
onGenerarReporte: (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}) => Promise<void>;
onCancel: () => void;
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteMovimientoBobinasEstado: React.FC<SeleccionaReporteMovimientoBobinasEstadoProps> = ({
onGenerarReporte,
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 [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();
}, []);
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 (!idPlanta) {
errors.idPlanta = 'Debe seleccionar una planta.';
}
setLocalErrors(errors);
return Object.keys(errors).length === 0;
};
const handleGenerar = () => {
if (!validate()) return;
onGenerarReporte({
fechaDesde,
fechaHasta,
idPlanta: Number(idPlanta)
});
};
return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
<Typography variant="h6" gutterBottom>
Parámetros: Movimiento de Bobinas por Estado
</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 }}
/>
<FormControl fullWidth margin="normal" error={!!localErrors.idPlanta} disabled={isLoading || loadingDropdowns}>
<InputLabel id="planta-select-label-estado" required>Planta</InputLabel>
<Select
labelId="planta-select-label-estado"
label="Planta"
value={idPlanta}
onChange={(e) => { setIdPlanta(e.target.value as number); setLocalErrors(p => ({ ...p, idPlanta: null })); }}
>
<MenuItem value="" disabled><em>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={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
</Button>
</Box>
</Box>
);
};
export default SeleccionaReporteMovimientoBobinasEstado;

View File

@@ -53,8 +53,12 @@ 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 ReportesIndexPage from '../pages/Reportes/ReportesIndexPage';
import ReporteExistenciaPapelPage from '../pages/Reportes/ReporteExistenciaPapelPage';
import ReporteMovimientoBobinasPage from '../pages/Reportes/ReporteMovimientoBobinasPage';
import ReporteMovimientoBobinasEstadoPage from '../pages/Reportes/ReporteMovimientoBobinasEstadoPage';
import ReporteListadoDistribucionGeneralPage from '../pages/Reportes/ReporteListadoDistribucionGeneralPage';
import ReporteListadoDistribucionCanillasPage from '../pages/Reportes/ReporteListadoDistribucionCanillasPage';
// Auditorias
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
@@ -153,7 +157,10 @@ const AppRoutes = () => {
<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 path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />
<Route path="listado-distribucion-general" element={<ReporteListadoDistribucionGeneralPage />} />
<Route path="listado-distribucion-canillas" element={<ReporteListadoDistribucionCanillasPage />} />
</Route>
{/* Módulo de Radios (anidado) */}

View File

@@ -1,5 +1,9 @@
import apiClient from '../apiClient';
import type { ExistenciaPapelDto } from '../../models/dtos/Reportes/ExistenciaPapelDto';
import type { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto';
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
import type { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto';
interface GetExistenciaPapelParams {
fechaDesde: string; // yyyy-MM-dd
@@ -40,13 +44,102 @@ const getExistenciaPapel = async (params: GetExistenciaPapelParams): Promise<Exi
return response.data;
};
const getMovimientoBobinas = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}): Promise<MovimientoBobinasDto[]> => {
const response = await apiClient.get<MovimientoBobinasDto[]>('/reportes/movimiento-bobinas', { params });
return response.data;
};
// ... Aquí irán los métodos para otros reportes ...
const getMovimientoBobinasPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/movimiento-bobinas/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getMovimientoBobinasEstado = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}): Promise<MovimientoBobinasPorEstadoResponseDto> => { // <- Devuelve el DTO combinado
const response = await apiClient.get<MovimientoBobinasPorEstadoResponseDto>('/reportes/movimiento-bobinas-estado', { params });
return response.data;
};
const getMovimientoBobinasEstadoPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/movimiento-bobinas-estado/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getListadoDistribucionGeneral = async (params: {
idPublicacion: number;
fechaDesde: string; // YYYY-MM-DD (primer día del mes)
fechaHasta: string; // YYYY-MM-DD (último día del mes)
}): Promise<ListadoDistribucionGeneralResponseDto> => {
const response = await apiClient.get<ListadoDistribucionGeneralResponseDto>('/reportes/listado-distribucion-general', { params });
return response.data;
};
const getListadoDistribucionGeneralPdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-general/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getListadoDistribucionCanillas = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<ListadoDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ListadoDistribucionCanillasResponseDto>('/reportes/listado-distribucion-canillas', { params });
return response.data;
};
const getListadoDistribucionCanillasPdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-canillas/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const reportesService = {
getExistenciaPapel,
getExistenciaPapelPdf,
// ...
getMovimientoBobinas,
getMovimientoBobinasPdf,
getMovimientoBobinasEstado,
getMovimientoBobinasEstadoPdf,
getListadoDistribucionGeneral,
getListadoDistribucionGeneralPdf,
getListadoDistribucionCanillas,
getListadoDistribucionCanillasPdf
};
export default reportesService;