Continuidad de reportes Frontend. Se sigue..
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ListadoDistribucionCanillasSimpleDto } from './ListadoDistribucionCanillasSimpleDto';
|
||||
import type { ListadoDistribucionCanillasPromedioDiaDto } from './ListadoDistribucionCanillasPromedioDiaDto';
|
||||
|
||||
export interface ListadoDistribucionCanillasResponseDto {
|
||||
detalleSimple: ListadoDistribucionCanillasSimpleDto[];
|
||||
promediosPorDia: ListadoDistribucionCanillasPromedioDiaDto[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface ListadoDistribucionCanillasSimpleDto {
|
||||
dia: number; // Día del mes
|
||||
llevados: number;
|
||||
devueltos: number;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface ListadoDistribucionGeneralPromedioDiaDto {
|
||||
dia: string;
|
||||
cantidadDias: number;
|
||||
promedioTirada: number;
|
||||
promedioSinCargo: number;
|
||||
promedioPerdidos: number;
|
||||
promedioLlevados: number;
|
||||
promedioDevueltos: number;
|
||||
promedioVendidos: number;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ListadoDistribucionGeneralResumenDto } from './ListadoDistribucionGeneralResumenDto';
|
||||
import type { ListadoDistribucionGeneralPromedioDiaDto } from './ListadoDistribucionGeneralPromedioDiaDto';
|
||||
|
||||
export interface ListadoDistribucionGeneralResponseDto {
|
||||
resumen: ListadoDistribucionGeneralResumenDto[];
|
||||
promediosPorDia: ListadoDistribucionGeneralPromedioDiaDto[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface MovimientoBobinaEstadoTotalDto {
|
||||
tipoMovimiento: string; // "Ingresos", "Utilizadas", "Dañadas"
|
||||
totalBobinas: number;
|
||||
totalKilos: number;
|
||||
}
|
||||
13
Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts
Normal file
13
Frontend/src/models/dtos/Reportes/MovimientoBobinasDto.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MovimientoBobinaEstadoDetalleDto } from "./MovimientoBobinaEstadoDetalleDto";
|
||||
import type { MovimientoBobinaEstadoTotalDto } from "./MovimientoBobinaEstadoTotalDto";
|
||||
|
||||
export interface MovimientoBobinasPorEstadoResponseDto {
|
||||
detalle: MovimientoBobinaEstadoDetalleDto[];
|
||||
totales: MovimientoBobinaEstadoTotalDto[];
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
212
Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx
Normal file
212
Frontend/src/pages/Reportes/ReporteMovimientoBobinasPage.tsx
Normal 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;
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) */}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user