Ajustes de reportes y controles.

Se implementan DataGrid a los reportes y se mejoran los controles de selección y presentación.
This commit is contained in:
2025-05-31 23:48:42 -03:00
parent 1182a4cdee
commit 99532b03f1
35 changed files with 4132 additions and 1363 deletions

View File

@@ -111,7 +111,7 @@ namespace GestionIntegral.Api.Controllers
{
LocalReport report = new LocalReport();
string rdlcPath = consolidado ? "Controllers/Reportes/RDLC/ReporteExistenciaPapelConsolidado.rdlc" : "Controllers/Reportes/RDLC/ReporteExistenciaPapel.rdlc";
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
report.LoadReportDefinition(fs);
@@ -121,24 +121,28 @@ namespace GestionIntegral.Api.Controllers
var parameters = new List<ReportParameter>();
parameters.Add(new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")));
parameters.Add(new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")));
string nombrePlantaParam = "Consolidado";
if (!consolidado && idPlanta.HasValue)
{
var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
nombrePlantaParam = planta?.Nombre ?? "N/A";
} else if (consolidado) {
// Para el consolidado, el RDLC ReporteExistenciaPapelConsolidado.txt NO espera NomPlanta
}
else { // No consolidado pero idPlanta es NULL (aunque el servicio ya valida esto)
else if (consolidado)
{
// Para el consolidado, el RDLC ReporteExistenciaPapelConsolidado.txt NO espera NomPlanta
}
else
{ // No consolidado pero idPlanta es NULL (aunque el servicio ya valida esto)
nombrePlantaParam = "N/A";
}
// Solo añadir NomPlanta si NO es consolidado, porque el RDLC consolidado no lo tiene.
if (!consolidado) {
parameters.Add(new ReportParameter("NomPlanta", nombrePlantaParam));
if (!consolidado)
{
parameters.Add(new ReportParameter("NomPlanta", nombrePlantaParam));
}
report.SetParameters(parameters);
byte[] pdfBytes = report.Render("PDF");
@@ -165,7 +169,7 @@ namespace GestionIntegral.Api.Controllers
var (data, error) = await _reportesService.ObtenerMovimientoBobinasAsync(fechaDesde, fechaHasta, idPlanta); // <--- CORREGIDO
if (error != null) return BadRequest(new { message = error });
return Ok(data);
}
@@ -180,7 +184,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int idPlanta)
{
if (!TienePermiso(PermisoVerReporteMovimientoBobinas)) return Forbid();
var (data, error) = await _reportesService.ObtenerMovimientoBobinasAsync(fechaDesde, fechaHasta, idPlanta); // <--- CORREGIDO
if (error != null) return BadRequest(new { message = error });
@@ -203,7 +207,7 @@ namespace GestionIntegral.Api.Controllers
parameters.Add(new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")));
parameters.Add(new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")));
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A"));
report.SetParameters(parameters);
byte[] pdfBytes = report.Render("PDF");
@@ -233,10 +237,10 @@ namespace GestionIntegral.Api.Controllers
var (detalle, totales, error) = await _reportesService.ObtenerMovimientoBobinasPorEstadoAsync(fechaDesde, fechaHasta, idPlanta);
if (error != null) return BadRequest(new { message = error });
if ((detalle == null || !detalle.Any()) && (totales == null || !totales.Any()))
{
return NotFound(new { message = "No hay datos para el reporte de movimiento de bobinas por estado." });
return NotFound(new { message = "No hay datos para el reporte de movimiento de bobinas por estado." });
}
var response = new MovimientoBobinasPorEstadoResponseDto
@@ -244,7 +248,7 @@ namespace GestionIntegral.Api.Controllers
Detalle = detalle ?? Enumerable.Empty<MovimientoBobinaEstadoDetalleDto>(),
Totales = totales ?? Enumerable.Empty<MovimientoBobinaEstadoTotalDto>()
};
return Ok(response);
}
@@ -265,9 +269,9 @@ namespace GestionIntegral.Api.Controllers
if (error != null) return BadRequest(new { message = error });
if ((detalle == null || !detalle.Any()) && (totales == null || !totales.Any()))
{
return NotFound(new { message = "No hay datos para generar el PDF del movimiento de bobinas por estado con los parámetros seleccionados." });
return NotFound(new { message = "No hay datos para generar el PDF del movimiento de bobinas por estado con los parámetros seleccionados." });
}
try
{
LocalReport report = new LocalReport();
@@ -275,15 +279,15 @@ namespace GestionIntegral.Api.Controllers
{
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSMovimientoBobinasEstado", detalle ?? new List<MovimientoBobinaEstadoDetalleDto>()));
report.DataSources.Add(new ReportDataSource("DSMovimientoBobinasEstadoTotales", totales ?? new List<MovimientoBobinaEstadoTotalDto>()));
report.DataSources.Add(new ReportDataSource("DSMovimientoBobinasEstado", detalle ?? new List<MovimientoBobinaEstadoDetalleDto>()));
report.DataSources.Add(new ReportDataSource("DSMovimientoBobinasEstadoTotales", totales ?? new List<MovimientoBobinaEstadoTotalDto>()));
var planta = await _plantaRepository.GetByIdAsync(idPlanta);
var parameters = new List<ReportParameter>();
parameters.Add(new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")));
parameters.Add(new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")));
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A"));
report.SetParameters(parameters);
byte[] pdfBytes = report.Render("PDF");
@@ -305,7 +309,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetListadoDistribucionGeneral(
[FromQuery] int idPublicacion,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta)
{
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
@@ -397,7 +401,7 @@ namespace GestionIntegral.Api.Controllers
{
return NotFound(new { message = "No hay datos para el listado de distribución de canillas." });
}
var response = new ListadoDistribucionCanillasResponseDto
{
DetalleSimple = simple ?? Enumerable.Empty<ListadoDistribucionCanillasSimpleDto>(),
@@ -478,10 +482,10 @@ namespace GestionIntegral.Api.Controllers
{
return NotFound(new { message = "No hay datos para el listado de distribución de canillas con importe." });
}
return Ok(data);
}
[HttpGet("listado-distribucion-canillas-importe/pdf")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -491,7 +495,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int idPublicacion,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta,
[FromQuery] bool esAccionista)
[FromQuery] bool esAccionista)
{
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
@@ -506,7 +510,7 @@ namespace GestionIntegral.Api.Controllers
try
{
LocalReport report = new LocalReport();
using (var fs = new FileStream("Controllers/Reportes/RDLC/ReporteListadoDistribucionCanImp.rdlc", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var fs = new FileStream("Controllers/Reportes/RDLC/ReporteListadoDistribucionCanImp.rdlc", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
report.LoadReportDefinition(fs);
}
@@ -518,10 +522,10 @@ namespace GestionIntegral.Api.Controllers
new ReportParameter("NomPubli", publicacion?.Nombre ?? "N/A"),
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")),
new ReportParameter("CanAcc", esAccionista ? "1" : "0")
new ReportParameter("CanAcc", esAccionista ? "1" : "0")
};
report.SetParameters(parameters);
byte[] pdfBytes = report.Render("PDF");
string tipoVendedor = esAccionista ? "Accionistas" : "Canillitas";
string fileName = $"ListadoDistCanImp_Pub{idPublicacion}_{tipoVendedor}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf";
@@ -565,7 +569,7 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", data)); // Basado en el RDLC
report.SetParameters(new[] {
report.SetParameters(new[] {
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
});
@@ -597,7 +601,7 @@ namespace GestionIntegral.Api.Controllers
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte." });
try
{
LocalReport report = new LocalReport();
@@ -606,7 +610,7 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", data)); // Basado en el RDLC
report.SetParameters(new[] {
report.SetParameters(new[] {
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
});
@@ -630,7 +634,7 @@ namespace GestionIntegral.Api.Controllers
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." });
return Ok(data);
}
[HttpGet("venta-mensual-secretaria/tirada-devolucion/pdf")]
public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{
@@ -638,7 +642,7 @@ namespace GestionIntegral.Api.Controllers
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte." });
try
{
LocalReport report = new LocalReport();
@@ -647,7 +651,7 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", data)); // Basado en el RDLC
report.SetParameters(new[] {
report.SetParameters(new[] {
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
});
@@ -667,12 +671,12 @@ namespace GestionIntegral.Api.Controllers
{
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) =
var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) =
await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
if (error != null) return BadRequest(new { message = error });
// Una validación simple, podrías hacerla más granular si es necesario
bool noHayDatos = (canillas == null || !canillas.Any()) &&
(canillasAcc == null || !canillasAcc.Any()) &&
@@ -695,10 +699,10 @@ namespace GestionIntegral.Api.Controllers
ControlDevolucionesDetalle = ctrlDevolucionesParaDistCan ?? Enumerable.Empty<ControlDevolucionesReporteDto>(),
ControlDevolucionesOtrosDias = ctrlDevolucionesOtrosDias ?? Enumerable.Empty<DevueltosOtrosDiasDto>()
};
return Ok(response);
}
[HttpGet("distribucion-canillas/pdf")]
public async Task<IActionResult> GetReporteDistribucionCanillasPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa, [FromQuery] bool soloTotales = false)
{
@@ -706,24 +710,24 @@ namespace GestionIntegral.Api.Controllers
// CORRECCIÓN AQUÍ: Añadir la variable para el nuevo elemento de la tupla
var (
canillas,
canillasAcc,
canillasAll,
canillasFechaLiq,
canillasAccFechaLiq,
canillas,
canillasAcc,
canillasAll,
canillasFechaLiq,
canillasAccFechaLiq,
ctrlDevolucionesRemitos, // Renombrado para claridad
ctrlDevolucionesParaDistCan,
_ , // Descartamos ctrlDevolucionesOtrosDias si no se usa aquí
ctrlDevolucionesParaDistCan,
_, // Descartamos ctrlDevolucionesOtrosDias si no se usa aquí
error
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
if (error != null) return BadRequest(new { message = error });
// La lógica de noHayDatosParaTotales y noHayDatosParaDetalle debería usar los nombres correctos
bool noHayDatosParaTotales = (canillasAll == null || !canillasAll.Any()) &&
(ctrlDevolucionesRemitos == null || !ctrlDevolucionesRemitos.Any()) && // Usar ctrlDevolucionesRemitos o ctrlDevolucionesParaDistCan según el RDLC
(ctrlDevolucionesParaDistCan == null || !ctrlDevolucionesParaDistCan.Any());
bool noHayDatosParaDetalle = noHayDatosParaTotales &&
(canillas == null || !canillas.Any()) &&
(canillasAcc == null || !canillasAcc.Any()) &&
@@ -739,12 +743,12 @@ namespace GestionIntegral.Api.Controllers
{
LocalReport report = new LocalReport();
string rdlcPath = soloTotales ? "Controllers/Reportes/RDLC/ReporteDistribucionCanillasTotales.rdlc" : "Controllers/Reportes/RDLC/ReporteDistribucionCanillas.rdlc";
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCan", canillas ?? new List<DetalleDistribucionCanillaDto>()));
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCanAcc", canillasAcc ?? new List<DetalleDistribucionCanillaDto>()));
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCanALL", canillasAll ?? new List<DetalleDistribucionCanillaAllDto>()));
@@ -796,7 +800,7 @@ namespace GestionIntegral.Api.Controllers
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); // Reutilizamos este método
if (error != null) return BadRequest(new { message = error });
// Adaptar la condición de "no hay datos" a los DataSets que realmente usa este reporte
if ((ctrlDevolucionesParaDistCanData == null || !ctrlDevolucionesParaDistCanData.Any()) &&
(ctrlDevolucionesOtrosDiasData == null || !ctrlDevolucionesOtrosDiasData.Any()) &&
@@ -811,18 +815,18 @@ namespace GestionIntegral.Api.Controllers
DevolucionesOtrosDias = ctrlDevolucionesOtrosDiasData ?? Enumerable.Empty<DevueltosOtrosDiasDto>(),
RemitosIngresados = ctrlDevolucionesRemitosData ?? Enumerable.Empty<ObtenerCtrlDevolucionesDto>()
};
return Ok(response);
}
[HttpGet("control-devoluciones/pdf")]
public async Task<IActionResult> GetReporteControlDevolucionesPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa)
{
if (!TienePermiso(PermisoVerControlDevoluciones)) return Forbid();
if (!TienePermiso(PermisoVerControlDevoluciones)) return Forbid();
// La tupla ahora devuelve un campo más
var (
_, _, _, _, _,
_, _, _, _, _,
ctrlDevolucionesRemitosData, // Para DSObtenerCtrlDevoluciones
ctrlDevolucionesParaDistCanData, // Para DSCtrlDevoluciones
ctrlDevolucionesOtrosDiasData, // <--- NUEVO: Para DSCtrlDevolucionesOtrosDias
@@ -830,16 +834,16 @@ namespace GestionIntegral.Api.Controllers
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
if (error != null) return BadRequest(new { message = error });
// Ajustamos la condición para verificar los DataSets que realmente usa este reporte específico
if ((ctrlDevolucionesRemitosData == null || !ctrlDevolucionesRemitosData.Any()) &&
if ((ctrlDevolucionesRemitosData == null || !ctrlDevolucionesRemitosData.Any()) &&
(ctrlDevolucionesParaDistCanData == null || !ctrlDevolucionesParaDistCanData.Any()) &&
(ctrlDevolucionesOtrosDiasData == null || !ctrlDevolucionesOtrosDiasData.Any()) // <--- AÑADIDO A LA VERIFICACIÓN
)
{
return NotFound(new { message = "No hay datos para generar el PDF para control de devoluciones." });
}
try
{
LocalReport report = new LocalReport();
@@ -847,13 +851,13 @@ namespace GestionIntegral.Api.Controllers
{
report.LoadReportDefinition(fs);
}
// DataSet que usa SP_DistCanillasCantidadEntradaSalida
report.DataSources.Add(new ReportDataSource("DSCtrlDevoluciones", ctrlDevolucionesParaDistCanData ?? new List<ControlDevolucionesReporteDto>()));
// DataSet que usa SP_DistCanillasCantidadEntradaSalidaOtrosDias
report.DataSources.Add(new ReportDataSource("DSCtrlDevolucionesOtrosDias", ctrlDevolucionesOtrosDiasData ?? new List<DevueltosOtrosDiasDto>())); // <--- CORREGIDO
// DataSet que usa SP_ObtenerCtrlDevoluciones
report.DataSources.Add(new ReportDataSource("DSObtenerCtrlDevoluciones", ctrlDevolucionesRemitosData ?? new List<ObtenerCtrlDevolucionesDto>()));
@@ -891,15 +895,15 @@ namespace GestionIntegral.Api.Controllers
{
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && !saldos.Any())
{
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
}
var distribuidor = await _distribuidorRepository.GetByIdAsync(idDistribuidor);
var empresa = await _empresaRepository.GetByIdAsync(idEmpresa);
@@ -912,7 +916,7 @@ namespace GestionIntegral.Api.Controllers
NombreDistribuidor = distribuidor.Distribuidor?.Nombre,
NombreEmpresa = empresa?.Nombre
};
return Ok(response);
}
@@ -925,13 +929,13 @@ namespace GestionIntegral.Api.Controllers
{
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && !saldos.Any())
{
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
}
try
@@ -946,7 +950,7 @@ namespace GestionIntegral.Api.Controllers
report.DataSources.Add(new ReportDataSource("DSDistribuidoresDebCred", debitosCreditos ?? new List<BalanceCuentaDebCredDto>()));
report.DataSources.Add(new ReportDataSource("DSDistribuidoresPagos", pagos ?? new List<BalanceCuentaPagosDto>()));
report.DataSources.Add(new ReportDataSource("DSDistribuidoresSaldos", saldos ?? new List<SaldoDto>()));
var parameters = new List<ReportParameter>
{
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
@@ -973,9 +977,9 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetReporteTiradasPublicacionesSeccionesData(
[FromQuery] int idPublicacion,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta,
[FromQuery] int? idPlanta,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta,
[FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false)
{
if (!TienePermiso(PermisoVerReporteTiradas)) return Forbid();
@@ -992,7 +996,7 @@ namespace GestionIntegral.Api.Controllers
if (!idPlanta.HasValue) return BadRequest(new { message = "Se requiere IdPlanta para reportes no consolidados." });
(data, errorMsg) = await _reportesService.ObtenerTiradasPublicacionesSeccionesAsync(idPublicacion, fechaDesde, fechaHasta, idPlanta.Value);
}
if (errorMsg != null) return BadRequest(new { message = errorMsg });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
@@ -1002,9 +1006,9 @@ namespace GestionIntegral.Api.Controllers
[HttpGet("tiradas-publicaciones-secciones/pdf")]
public async Task<IActionResult> GetReporteTiradasPublicacionesSeccionesPdf(
[FromQuery] int idPublicacion,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta,
[FromQuery] int? idPlanta,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta,
[FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false)
{
if (!TienePermiso(PermisoVerReporteTiradas)) return Forbid();
@@ -1021,15 +1025,15 @@ namespace GestionIntegral.Api.Controllers
if (!idPlanta.HasValue) return BadRequest(new { message = "Se requiere IdPlanta para reportes no consolidados." });
(data, errorMsg) = await _reportesService.ObtenerTiradasPublicacionesSeccionesAsync(idPublicacion, fechaDesde, fechaHasta, idPlanta.Value);
}
if (errorMsg != null) return BadRequest(new { message = errorMsg });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
try
{
LocalReport report = new LocalReport();
string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteTiradasPublicacionesSeccionesConsolidado.rdlc" :
string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteTiradasPublicacionesSeccionesConsolidado.rdlc" :
"Controllers/Reportes/RDLC/ReporteTiradasPublicacionesSecciones.rdlc";
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
@@ -1041,7 +1045,7 @@ namespace GestionIntegral.Api.Controllers
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(idPublicacion);
var parameters = new List<ReportParameter>
{
new ReportParameter("Mes", fechaDesde.ToString("MMMM yyyy")),
new ReportParameter("Mes", fechaDesde.ToString("MMMM yyyy")),
new ReportParameter("NomPubli", publicacion?.Nombre ?? "N/A")
};
if (!consolidado && idPlanta.HasValue)
@@ -1049,7 +1053,7 @@ namespace GestionIntegral.Api.Controllers
var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A"));
}
report.SetParameters(parameters);
byte[] pdfBytes = report.Render("PDF");
@@ -1071,7 +1075,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false)
{
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
IEnumerable<ConsumoBobinasSeccionDto> data;
string? errorMsg;
@@ -1085,10 +1089,10 @@ namespace GestionIntegral.Api.Controllers
if (!idPlanta.HasValue) return BadRequest(new { message = "Se requiere IdPlanta para reportes no consolidados." });
(data, errorMsg) = await _reportesService.ObtenerConsumoBobinasPorSeccionAsync(fechaDesde, fechaHasta, idPlanta.Value);
}
if (errorMsg != null) return BadRequest(new { message = errorMsg });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
return Ok(data);
}
@@ -1099,7 +1103,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false)
{
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
IEnumerable<ConsumoBobinasSeccionDto> data;
string? errorMsg;
@@ -1113,28 +1117,28 @@ namespace GestionIntegral.Api.Controllers
if (!idPlanta.HasValue) return BadRequest(new { message = "Se requiere IdPlanta para reportes no consolidados." });
(data, errorMsg) = await _reportesService.ObtenerConsumoBobinasPorSeccionAsync(fechaDesde, fechaHasta, idPlanta.Value);
}
if (errorMsg != null) return BadRequest(new { message = errorMsg });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
try
{
LocalReport report = new LocalReport();
string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccionConsolidado.rdlc" :
"Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccion.rdlc";
string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccionConsolidado.rdlc" :
"Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccion.rdlc";
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSConsumoBobinasSeccion", data));
var parameters = new List<ReportParameter>
{
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
};
if (!consolidado && idPlanta.HasValue)
if (!consolidado && idPlanta.HasValue)
{
var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A"));
@@ -1158,10 +1162,10 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta)
{
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
var (data, error) = await _reportesService.ObtenerConsumoBobinasPorPublicacionAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
@@ -1173,10 +1177,10 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta)
{
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
var (data, error) = await _reportesService.ObtenerConsumoBobinasPorPublicacionAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
@@ -1188,7 +1192,7 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSConsumoBobinasPublicacion", data));
var parameters = new List<ReportParameter>
{
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
@@ -1215,7 +1219,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false)
{
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
IEnumerable<ComparativaConsumoBobinasDto> data;
string? errorMsg;
@@ -1229,7 +1233,7 @@ namespace GestionIntegral.Api.Controllers
if (!idPlanta.HasValue) return BadRequest(new { message = "Se requiere IdPlanta para reportes no consolidados." });
(data, errorMsg) = await _reportesService.ObtenerComparativaConsumoBobinasAsync(fechaInicioMesA, fechaFinMesA, fechaInicioMesB, fechaFinMesB, idPlanta.Value);
}
if (errorMsg != null) return BadRequest(new { message = errorMsg });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
@@ -1243,7 +1247,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false)
{
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
IEnumerable<ComparativaConsumoBobinasDto> data;
string? errorMsg;
@@ -1257,15 +1261,15 @@ namespace GestionIntegral.Api.Controllers
if (!idPlanta.HasValue) return BadRequest(new { message = "Se requiere IdPlanta para reportes no consolidados." });
(data, errorMsg) = await _reportesService.ObtenerComparativaConsumoBobinasAsync(fechaInicioMesA, fechaFinMesA, fechaInicioMesB, fechaFinMesB, idPlanta.Value);
}
if (errorMsg != null) return BadRequest(new { message = errorMsg });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el reporte." });
try
{
LocalReport report = new LocalReport();
string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteConsumoBobinasMesesConsolidado.rdlc" :
string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteConsumoBobinasMesesConsolidado.rdlc" :
"Controllers/Reportes/RDLC/ReporteConsumoBobinasMeses.rdlc";
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
@@ -1273,13 +1277,13 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs);
}
report.DataSources.Add(new ReportDataSource("DSConsumoBobinasMeses", data));
var parameters = new List<ReportParameter>
{
new ReportParameter("MesA", fechaInicioMesA.ToString("MMMM yyyy")),
new ReportParameter("MesB", fechaInicioMesB.ToString("MMMM yyyy"))
};
if (!consolidado && idPlanta.HasValue)
if (!consolidado && idPlanta.HasValue)
{
var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A"));
@@ -1292,5 +1296,93 @@ namespace GestionIntegral.Api.Controllers
}
catch (Exception ex) { _logger.LogError(ex, "Error PDF Comparativa Consumo Bobinas."); return StatusCode(500, "Error interno."); }
}
// GET: api/reportes/listado-distribucion-distribuidores
[HttpGet("listado-distribucion-distribuidores")]
[ProducesResponseType(typeof(ListadoDistribucionDistribuidoresResponseDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetListadoDistribucionDistribuidores(
[FromQuery] int idDistribuidor,
[FromQuery] int idPublicacion,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta)
{
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // RR002
(IEnumerable<ListadoDistribucionDistSimpleDto> simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> promedios, string? error) result =
await _reportesService.ObtenerListadoDistribucionDistribuidoresAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
if (result.error != null) return BadRequest(new { message = result.error });
if ((result.simple == null || !result.simple.Any()) && (result.promedios == null || !result.promedios.Any()))
{
return NotFound(new { message = "No hay datos para el listado de distribución de distribuidores." });
}
var response = new ListadoDistribucionDistribuidoresResponseDto
{
DetalleSimple = result.simple ?? Enumerable.Empty<ListadoDistribucionDistSimpleDto>(),
PromediosPorDia = result.promedios ?? Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>()
};
return Ok(response);
}
[HttpGet("listado-distribucion-distribuidores/pdf")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetListadoDistribucionDistribuidoresPdf(
[FromQuery] int idDistribuidor,
[FromQuery] int idPublicacion,
[FromQuery] DateTime fechaDesde,
[FromQuery] DateTime fechaHasta)
{
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
var (simple, promedios, error) = await _reportesService.ObtenerListadoDistribucionDistribuidoresAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error });
if ((simple == null || !simple.Any()) && (promedios == null || !promedios.Any()))
{
return NotFound(new { message = "No hay datos para generar el PDF." });
}
try
{
LocalReport report = new LocalReport();
// USAREMOS EL MISMO RDLC QUE PARA CANILLITAS, YA QUE TIENE LA MISMA ESTRUCTURA DE DATASOURCES
using (var fs = new FileStream("Controllers/Reportes/RDLC/ReporteListadoDistribucion.rdlc", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
report.LoadReportDefinition(fs);
}
// Nombres de DataSources deben coincidir con los del RDLC
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", simple ?? new List<ListadoDistribucionDistSimpleDto>()));
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionAgDias", promedios ?? new List<ListadoDistribucionDistPromedioDiaDto>()));
var publicacionData = await _publicacionRepository.GetByIdAsync(idPublicacion);
var distribuidorData = await _distribuidorRepository.GetByIdAsync(idDistribuidor);
var parameters = new List<ReportParameter>
{
new ReportParameter("NomPubli", publicacionData.Publicacion?.Nombre ?? "N/A"),
new ReportParameter("NomDist", distribuidorData.Distribuidor?.Nombre ?? "N/A"), // Parámetro para el RDLC
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
};
report.SetParameters(parameters);
byte[] pdfBytes = report.Render("PDF");
string fileName = $"ListadoDistribucion_Dist{idDistribuidor}_Pub{idPublicacion}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf";
return File(pdfBytes, "application/pdf", fileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al generar PDF para Listado Distribucion (Distribuidores).");
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del reporte.");
}
}
}
}

View File

@@ -11,8 +11,6 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
Task<IEnumerable<MovimientoBobinasDto>> GetMovimientoBobinasAsync(DateTime fechaInicio, int diasPeriodo, int idPlanta);
Task<IEnumerable<MovimientoBobinaEstadoDetalleDto>> GetMovimientoBobinasEstadoDetalleAsync(DateTime fechaInicio, DateTime fechaFin, int idPlanta);
Task<IEnumerable<MovimientoBobinaEstadoTotalDto>> GetMovimientoBobinasEstadoTotalesAsync(DateTime fechaInicio, DateTime fechaFin, int idPlanta);
// --- MÉTODOS AÑADIDOS AQUÍ ---
Task<IEnumerable<ListadoDistribucionGeneralResumenDto>> GetListadoDistribucionGeneralResumenAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<ListadoDistribucionGeneralPromedioDiaDto>> GetListadoDistribucionGeneralPromedioDiaAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<ListadoDistribucionCanillasSimpleDto>> GetListadoDistribucionCanillasSimpleAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
@@ -40,5 +38,8 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
Task<IEnumerable<BalanceCuentaDebCredDto>> GetBalanceCuentDistDebCredEmpresaAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<BalanceCuentaPagosDto>> GetBalanceCuentDistPagosEmpresaAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<SaldoDto>> GetBalanceCuentSaldosEmpresasAsync(string destino, int idDestino, int idEmpresa);
Task<IEnumerable<ListadoDistribucionDistSimpleDto>> GetListadoDistribucionDistSimpleAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<ListadoDistribucionDistPromedioDiaDto>> GetListadoDistribucionDistPromedioDiaAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
}
}

View File

@@ -132,7 +132,6 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
}
}
// Implementaciones que faltaban
public async Task<IEnumerable<ListadoDistribucionGeneralResumenDto>> GetListadoDistribucionGeneralResumenAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
{
const string spName = "dbo.SP_DistObtenerResumenMensual";
@@ -419,5 +418,66 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync<SaldoDto>(spName, parameters, commandType: CommandType.StoredProcedure); }
catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty<SaldoDto>(); }
}
public async Task<IEnumerable<ListadoDistribucionDistSimpleDto>> GetListadoDistribucionDistSimpleAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
{
const string spName = "dbo.SP_CantidadEntradaSalida";
var parameters = new DynamicParameters();
parameters.Add("@idDistribuidor", idDistribuidor, DbType.Int32);
parameters.Add("@idPublicacion", idPublicacion, DbType.Int32);
parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime);
parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime);
try
{
using var connection = _dbConnectionFactory.CreateConnection(); // <--- CORREGIDO AQUÍ
return await connection.QueryAsync<ListadoDistribucionDistSimpleDto>(spName, parameters, commandType: CommandType.StoredProcedure);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al ejecutar SP {SPName} para Listado Distribucion Distribuidores (Simple). Params: Dist={idDistribuidor}, Pub={idPublicacion}, Desde={fechaDesde}, Hasta={fechaHasta}", spName, idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
return Enumerable.Empty<ListadoDistribucionDistSimpleDto>();
}
}
public async Task<IEnumerable<ListadoDistribucionDistPromedioDiaDto>> GetListadoDistribucionDistPromedioDiaAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
{
const string spName = "dbo.SP_CantidadEntradaSalidaCPromAgDia";
var parameters = new DynamicParameters();
parameters.Add("@idDistribuidor", idDistribuidor, DbType.Int32);
parameters.Add("@idPublicacion", idPublicacion, DbType.Int32);
parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime);
parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime);
try
{
using var connection = _dbConnectionFactory.CreateConnection(); // <--- CORREGIDO AQUÍ
return await connection.QueryAsync<ListadoDistribucionDistPromedioDiaDto>(spName, parameters, commandType: CommandType.StoredProcedure);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al ejecutar SP {SPName} para Listado Distribucion Distribuidores (Promedios). Params: Dist={idDistribuidor}, Pub={idPublicacion}, Desde={fechaDesde}, Hasta={fechaHasta}", spName, idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
return Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>();
}
}
public async Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
{
if (fechaDesde > fechaHasta)
return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "La fecha 'Desde' no puede ser mayor que la fecha 'Hasta'.");
try
{
var simpleDataTask = this.GetListadoDistribucionDistSimpleAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
var promediosDataTask = this.GetListadoDistribucionDistPromedioDiaAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
await Task.WhenAll(simpleDataTask, promediosDataTask);
return (await simpleDataTask, await promediosDataTask, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en ReportesService al obtener Listado Distribucion (Distribuidores).");
return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte.");
}
}
}
}

View File

@@ -0,0 +1,14 @@
namespace GestionIntegral.Api.Dtos.Reportes
{
public class ListadoDistribucionDistPromedioDiaDto
{
public string Dia { get; set; } = string.Empty; // Nombre del día de la semana (Lunes, Martes, etc.)
public int? Cant { get; set; } // Cantidad de días con ese nombre en el período
public int? Llevados { get; set; }
public int? Devueltos { get; set; }
public int? Promedio_Llevados { get; set; }
public int? Promedio_Devueltos { get; set; }
public int? Promedio_Ventas { get; set; }
// public int Dia_Orden { get; set; } // Si el SP devuelve Dia_Orden para ordenar
}
}

View File

@@ -0,0 +1,9 @@
namespace GestionIntegral.Api.Dtos.Reportes
{
public class ListadoDistribucionDistSimpleDto
{
public int Dia { get; set; } // Día del mes
public int? Llevados { get; set; } // Nullable si el SP puede devolver NULL
public int? Devueltos { get; set; } // Nullable si el SP puede devolver NULL
}
}

View File

@@ -0,0 +1,8 @@
namespace GestionIntegral.Api.Dtos.Reportes
{
public class ListadoDistribucionDistribuidoresResponseDto
{
public IEnumerable<ListadoDistribucionDistSimpleDto> DetalleSimple { get; set; } = Enumerable.Empty<ListadoDistribucionDistSimpleDto>();
public IEnumerable<ListadoDistribucionDistPromedioDiaDto> PromediosPorDia { get; set; } = Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>();
}
}

View File

@@ -61,5 +61,7 @@ namespace GestionIntegral.Api.Services.Reportes
IEnumerable<SaldoDto> Saldos,
string? Error
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
}
}

View File

@@ -427,5 +427,27 @@ namespace GestionIntegral.Api.Services.Reportes
);
}
}
public async Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
{
if (fechaDesde > fechaHasta)
return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "La fecha 'Desde' no puede ser mayor que la fecha 'Hasta'.");
try
{
// Llamar a los métodos específicos del repositorio
var simpleDataTask = _reportesRepository.GetListadoDistribucionDistSimpleAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
var promediosDataTask = _reportesRepository.GetListadoDistribucionDistPromedioDiaAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
await Task.WhenAll(simpleDataTask, promediosDataTask);
return (await simpleDataTask, await promediosDataTask, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en ReportesService al obtener Listado Distribucion (Distribuidores). Params: Dist={idDistribuidor}, Pub={idPublicacion}, Desde={fechaDesde}, Hasta={fechaHasta}", idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte.");
}
}
}
}

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+70fc84772161b499c8283a31b7a61246a6bcc46f")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1182a4cdee4fcdb55dc3f2dbfeeb2ec2187f2bea")]
[assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","RFpydTIMf9fxYfNnrif0YQNHLgrUW58SpaLEeCZ9nXg="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","OZUau2FUwouOUoP6Eot2qiZlqRHSBBkSPL6vHtWUfGI="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","RFpydTIMf9fxYfNnrif0YQNHLgrUW58SpaLEeCZ9nXg="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","OZUau2FUwouOUoP6Eot2qiZlqRHSBBkSPL6vHtWUfGI="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -5,38 +5,37 @@ import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordMod
import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual
interface MainLayoutProps {
children: ReactNode; // Esto será el <Outlet /> que renderiza las páginas del módulo
children: ReactNode;
}
// Definir los módulos y sus rutas base
const modules = [
{ label: 'Inicio', path: '/' },
{ label: 'Distribución', path: '/distribucion' }, // Asumiremos rutas base como /distribucion, /contables, etc.
{ label: 'Contables', path: '/contables' },
{ label: 'Impresión', path: '/impresion' },
{ label: 'Reportes', path: '/reportes' },
{ label: 'Radios', path: '/radios' },
{ label: 'Usuarios', path: '/usuarios' },
{ label: 'Inicio', path: '/' },
{ label: 'Distribución', path: '/distribucion' },
{ label: 'Contables', path: '/contables' },
{ label: 'Impresión', path: '/impresion' },
{ label: 'Reportes', path: '/reportes' },
{ label: 'Radios', path: '/radios' },
{ label: 'Usuarios', path: '/usuarios' },
];
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const {
user,
logout,
showForcedPasswordChangeModal,
// ... (resto de las props de useAuth) ...
isAuthenticated,
isPasswordChangeForced,
passwordChangeCompleted,
showForcedPasswordChangeModal,
setShowForcedPasswordChangeModal,
isAuthenticated
passwordChangeCompleted
} = useAuth();
const navigate = useNavigate();
const location = useLocation(); // Para obtener la ruta actual
// Estado para el tab seleccionado
const [selectedTab, setSelectedTab] = useState<number | false>(false);
// Efecto para sincronizar el tab seleccionado con la ruta actual
useEffect(() => {
const currentModulePath = modules.findIndex(module =>
location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/'))
@@ -44,14 +43,13 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
if (currentModulePath !== -1) {
setSelectedTab(currentModulePath);
} else if (location.pathname === '/') {
setSelectedTab(0); // Seleccionar "Inicio" si es la raíz
setSelectedTab(0);
} else {
setSelectedTab(false); // Ningún tab coincide (podría ser una sub-ruta no principal)
setSelectedTab(false);
}
}, [location.pathname]);
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
// ... (lógica de handleModalClose existente) ...
if (passwordChangedSuccessfully) {
passwordChangeCompleted();
} else {
@@ -65,54 +63,54 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setSelectedTab(newValue);
navigate(modules[newValue].path); // Navegar a la ruta base del módulo
navigate(modules[newValue].path);
};
// Si el modal de cambio de clave forzado está activo, no mostramos la navegación principal aún.
// El modal se superpone.
// Determinar si el módulo actual es el de Reportes
const isReportesModule = location.pathname.startsWith('/reportes');
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
// ... (lógica del modal forzado sin cambios) ...
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<ChangePasswordModal
<ChangePasswordModal
open={showForcedPasswordChangeModal}
onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced}
/>
{/* Podrías querer un fondo o layout mínimo aquí si el modal no es pantalla completa */}
/>
</Box>
);
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="static">
{/* ... (Toolbar y Tabs sin cambios) ... */}
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Sistema de Gestión - El Día
</Typography>
{user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>}
{isAuthenticated && !isPasswordChangeForced && (
<Button
<Button
color="inherit"
onClick={() => setShowForcedPasswordChangeModal(true)} // Ahora abre el modal
>
onClick={() => setShowForcedPasswordChangeModal(true)}
>
Cambiar Contraseña
</Button>
</Button>
)}
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
</Toolbar>
{/* Navegación Principal por Módulos */}
<Paper square elevation={0} > {/* Usamos Paper para un fondo consistente para los Tabs */}
<Paper square elevation={0} >
<Tabs
value={selectedTab}
onChange={handleTabChange}
indicatorColor="secondary" // O "primary"
textColor="inherit" // O "primary" / "secondary"
variant="scrollable" // Permite scroll si hay muchos tabs
scrollButtons="auto" // Muestra botones de scroll si es necesario
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
scrollButtons="auto"
aria-label="módulos principales"
sx={{ backgroundColor: 'primary.main', color: 'white' }} // Color de fondo para los tabs
sx={{ backgroundColor: 'primary.main', color: 'white' }}
>
{modules.map((module) => (
<Tab key={module.path} label={module.label} />
@@ -121,13 +119,15 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
</Paper>
</AppBar>
{/* Contenido del Módulo (renderizado por <Outlet /> en AppRoutes) */}
{/* Contenido del Módulo */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3, // Padding
// overflowY: 'auto' // Si el contenido del módulo es muy largo
py: isReportesModule ? 0 : 3, // Padding vertical condicional. Si es el módulo de Reportes, px es 0 si no 3
px: isReportesModule ? 0 : 3, // Padding horizontal condicional. Si es el módulo de Reportes, px es 0 si no 3
display: 'flex', // IMPORTANTE: Para que el hijo (ReportesIndexPage) pueda usar height: '100%'
flexDirection: 'column' // IMPORTANTE
}}
>
{children}
@@ -139,14 +139,11 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
</Typography>
</Box>
{/* Modal para cambio de clave opcional (no forzado) */}
{/* Si showForcedPasswordChangeModal es true pero isPasswordChangeForced es false,
se mostrará aquí también. */}
<ChangePasswordModal
<ChangePasswordModal
open={showForcedPasswordChangeModal}
onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced} // Esto controla el comportamiento del modal
/>
isFirstLogin={isPasswordChangeForced}
/>
</Box>
);
};

View File

@@ -0,0 +1,9 @@
import type { ControlDevolucionesReporteDto } from './ControlDevolucionesReporteDto';
import type { DevueltosOtrosDiasDto } from './DevueltosOtrosDiasDto';
import type { ObtenerCtrlDevolucionesDto } from './ObtenerCtrlDevolucionesDto';
export interface ControlDevolucionesDataResponseDto {
detallesCtrlDevoluciones: ControlDevolucionesReporteDto[];
devolucionesOtrosDias: DevueltosOtrosDiasDto[];
remitosIngresados: ObtenerCtrlDevolucionesDto[];
}

View File

@@ -0,0 +1,10 @@
export interface ListadoDistribucionDistPromedioDiaDto {
id?: string; // Para DataGrid
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,6 @@
export interface ListadoDistribucionDistSimpleDto {
id?: string; // Para DataGrid
dia: number; // Día del mes
llevados: number;
devueltos: number | null; // Puede ser null si no hay devoluciones
}

View File

@@ -0,0 +1,7 @@
import type { ListadoDistribucionDistSimpleDto } from './ListadoDistribucionDistSimpleDto';
import type { ListadoDistribucionDistPromedioDiaDto } from './ListadoDistribucionDistPromedioDiaDto';
export interface ListadoDistribucionDistribuidoresResponseDto {
detalleSimple: ListadoDistribucionDistSimpleDto[];
promediosPorDia: ListadoDistribucionDistPromedioDiaDto[];
}

View File

@@ -1,17 +1,23 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody,
TableFooter
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import type {Theme} from '@mui/material/styles';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
import SeleccionaReporteComparativaConsumoBobinas from './SeleccionaReporteComparativaConsumoBobinas';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Interfaz extendida para DataGrid
interface ComparativaConsumoBobinasDataGridDto extends ComparativaConsumoBobinasDto {
id: string;
}
const ReporteComparativaConsumoBobinasPage: React.FC = () => {
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDto[]>([]);
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDataGridDto[]>([]); // Usar tipo extendido
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -21,9 +27,14 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
nombrePlanta?: string; // Para el PDF
nombrePlanta?: string;
mesA?: string;
mesB?: string;
} | null>(null);
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const handleGenerarReporte = useCallback(async (params: {
fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string;
@@ -40,12 +51,24 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
const plantaData = await plantaService.getPlantaById(params.idPlanta);
plantaNombre = plantaData?.nombre ?? "N/A";
}
setCurrentParams({...params, nombrePlanta: plantaNombre});
// Formatear nombres de meses para el PDF
const formatMonthYear = (dateString: string) => {
const date = new Date(dateString + 'T00:00:00'); // Asegurar que se parsea como local
return date.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
};
setCurrentParams({
...params,
nombrePlanta: plantaNombre,
mesA: formatMonthYear(params.fechaInicioMesA),
mesB: formatMonthYear(params.fechaInicioMesB)
});
try {
const data = await reportesService.getComparativaConsumoBobinas(params);
setReportData(data);
if (data.length === 0) {
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.tipoBobina}-${index}` }));
setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -72,7 +95,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
alert("No hay datos para exportar.");
return;
}
const dataToExport = reportData.map(item => ({
const dataToExport = reportData.map(({ ...rest }) => rest).map(item => ({
"Tipo Bobina": item.tipoBobina,
"Cant. Mes A": item.bobinasUtilizadasMesA,
"Cant. Mes B": item.bobinasUtilizadasMesB,
@@ -82,7 +105,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
"Dif. Kg": item.diferenciaKilosUtilizados,
}));
// Totales
const totales = dataToExport.reduce((acc, row) => {
acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]);
acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]);
@@ -96,7 +118,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
"Dif. Kg": totales.difKg,
});
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => {
@@ -104,7 +125,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
return { wch: maxLen + 2 };
});
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "ComparativaConsumo");
let fileName = "ReporteComparativaConsumoBobinas";
@@ -141,6 +161,118 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
}
}, [currentParams]);
const columns: GridColDef<ComparativaConsumoBobinasDataGridDto>[] = [ // Tipar con la interfaz correcta
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 250, flex: 1.5 },
{ field: 'bobinasUtilizadasMesA', headerName: 'Cant. Mes A', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasUtilizadasMesB', headerName: 'Cant. Mes B', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'diferenciaBobinasUtilizadas', headerName: 'Dif. Cant.', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosUtilizadosMesA', headerName: 'Kg Mes A', type: 'number', width: 120, align: 'right', headerAlign: 'right', cellClassName: 'separator-left', headerClassName: 'separator-left', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosUtilizadosMesB', headerName: 'Kg Mes B', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'diferenciaKilosUtilizados', headerName: 'Dif. Kg', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
];
const rows = useMemo(() => reportData, [reportData]);
// Calcular totales para el footer
const totalesGenerales = useMemo(() => {
if (reportData.length === 0) return null;
return {
bobinasUtilizadasMesA: reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesA, 0),
bobinasUtilizadasMesB: reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesB, 0),
diferenciaBobinasUtilizadas: reportData.reduce((sum, item) => sum + item.diferenciaBobinasUtilizadas, 0),
kilosUtilizadosMesA: reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesA, 0),
kilosUtilizadosMesB: reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesB, 0),
diferenciaKilosUtilizados: reportData.reduce((sum, item) => sum + item.diferenciaKilosUtilizados, 0),
};
}, [reportData]);
// eslint-disable-next-line react/display-name
const CustomFooter = () => {
if (!totalesGenerales) return null;
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
const colConfig = columns.find(c => c.field === field);
// Ajustar anchos para los totales para que sean más compactos
let targetWidth: number | string = 'auto';
let targetMinWidth: number | string = 'auto';
if (isLabel) {
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.5) : 100; // Más corto para "TOTALES:"
targetMinWidth = 80;
} else if (colConfig) {
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.75) : 90; // 75% del ancho de columna, mínimo 70
targetMinWidth = 70;
}
const style: React.CSSProperties = {
minWidth: targetMinWidth,
width: targetWidth,
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
paddingRight: isLabel ? 1 : (field === 'diferenciaKilosUtilizados' ? 0 : 1), // pr en theme units
fontWeight: 'bold',
whiteSpace: 'nowrap',
};
// Aplicar el separador si es la columna 'kilosUtilizadosMesA'
if (field === 'kilosUtilizadosMesA') {
style.borderLeft = `2px solid grey`; // O theme.palette.divider
style.paddingLeft = '8px'; // Espacio después del separador
}
return style;
};
return (
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
flexShrink: 0,
overflow: 'hidden',
px:1,
}}>
<GridFooter
sx={{
borderTop: 'none',
width: 'auto',
'& .MuiToolbar-root': {
paddingLeft: 0,
paddingRight: 0,
},
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflowX: 'auto',
px:1,
flexShrink: 1,
}}>
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesA')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesA)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesB')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesB)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('diferenciaBobinasUtilizadas')}>{numberLocaleFormatter(totalesGenerales.diferenciaBobinasUtilizadas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesA')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesA)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesB')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesB)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('diferenciaKilosUtilizados')}>{numberLocaleFormatter(totalesGenerales.diferenciaKilosUtilizados)}</Typography>
</Box>
</GridFooterContainer>
);
};
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -161,7 +293,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Comparativa Consumo de Bobinas</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={reportData.length === 0 || !!error} size="small">
@@ -173,51 +305,36 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Tipo Bobina</TableCell>
<TableCell align="right">Cant. Mes A</TableCell>
<TableCell align="right">Cant. Mes B</TableCell>
<TableCell align="right">Dif. Cant.</TableCell>
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>Kg Mes A</TableCell>
<TableCell align="right">Kg Mes B</TableCell>
<TableCell align="right">Dif. Kg</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.map((row, idx) => (
<TableRow key={`${row.tipoBobina}-${idx}`}>
<TableCell>{row.tipoBobina}</TableCell>
<TableCell align="right">{row.bobinasUtilizadasMesA.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.bobinasUtilizadasMesB.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.diferenciaBobinasUtilizadas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>{row.kilosUtilizadosMesA.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.kilosUtilizadosMesB.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.diferenciaKilosUtilizados.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow sx={{backgroundColor: 'grey.300'}}>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>TOTALES:</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesA, 0).toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesB, 0).toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.diferenciaBobinasUtilizadas, 0).toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem', borderLeft: '2px solid grey'}}>{reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesA, 0).toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesB, 0).toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.diferenciaKilosUtilizados, 0).toLocaleString('es-AR')}</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
<Paper sx={{
width: '100%',
mt: 2,
'& .separator-left': {
borderLeft: (theme: Theme) => `2px solid ${theme.palette.divider}`,
},
}}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
slots={{ footer: CustomFooter }}
density="compact"
sx={{ height: 'calc(100vh - 300px)' }} // Ajusta esta altura según sea necesario
initialState={{
pagination: {
paginationModel: { pageSize: 10, page: 0 },
},
}}
pageSizeOptions={[5, 10, 25, 50]}
disableRowSelectionOnClick
// hideFooterSelectedRowCount // Ya se maneja en CustomFooter
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};

View File

@@ -1,26 +1,20 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody, TableFooter
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import {
DataGrid,
type GridColDef,
GridFooterContainer,
GridFooter
} from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto';
import SeleccionaReporteConsumoBobinasPublicacion from './SeleccionaReporteConsumoBobinasPublicacion';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Helper para agrupar los datos por Planta
const groupDataByPlanta = (data: ConsumoBobinasPublicacionDto[]) => {
return data.reduce((acc, current) => {
const plantaKey = current.nombrePlanta;
if (!acc[plantaKey]) {
acc[plantaKey] = [];
}
acc[plantaKey].push(current);
return acc;
}, {} as Record<string, ConsumoBobinasPublicacionDto[]>);
};
const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
const [reportData, setReportData] = useState<ConsumoBobinasPublicacionDto[]>([]);
const [loading, setLoading] = useState(false);
@@ -31,6 +25,7 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
const [currentParams, setCurrentParams] = useState<{
fechaDesde: string;
fechaHasta: string;
nombrePublicacion?: string; // Mantenido para nombre de archivo, no afecta al título DataGrid
} | null>(null);
const handleGenerarReporte = useCallback(async (params: {
@@ -45,8 +40,10 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
try {
const data = await reportesService.getConsumoBobinasPorPublicacion(params);
setReportData(data);
if (data.length === 0) {
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.nombrePlanta}-${item.nombrePublicacion}-${index}` }));
setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -69,28 +66,28 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
}, []);
const handleExportToExcel = useCallback(() => {
if (reportData.length === 0) {
if (reportData.length === 0) {
alert("No hay datos para exportar.");
return;
}
const dataToExport = reportData.map(item => ({
"Planta": item.nombrePlanta,
"Publicación": item.nombrePublicacion,
"Total Kilos": item.totalKilos,
"Cantidad Bobinas": item.cantidadBobinas,
const dataToExport = reportData.map(({ ...rest }) => ({
"Planta": rest.nombrePlanta,
"Publicación": rest.nombrePublicacion,
"Total Kilos": rest.totalKilos,
"Cantidad Bobinas": rest.cantidadBobinas,
}));
// Calcular totales generales
const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0);
const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0);
dataToExport.push({
"Planta": "TOTAL GENERAL",
"Publicación": "",
"Publicación": "", // Celda vacía para alineación
"Total Kilos": totalGeneralKilos,
"Cantidad Bobinas": totalGeneralBobinas,
});
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => {
@@ -117,7 +114,7 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
setLoadingPdf(true);
setError(null);
try {
const blob = await reportesService.getConsumoBobinasPorPublicacionPdf(currentParams);
const blob = await reportesService.getConsumoBobinasPorPublicacionPdf(currentParams); // solo fechaDesde, fechaHasta
if (blob.type === "application/json") {
const text = await blob.text();
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
@@ -134,10 +131,100 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
}
}, [currentParams]);
const groupedData = reportData.length > 0 ? groupDataByPlanta(reportData) : {};
const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0);
const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0);
const columns: GridColDef[] = [
{ field: 'nombrePlanta', headerName: 'Planta', width: 200, flex: 1 },
{ field: 'nombrePublicacion', headerName: 'Publicación', width: 250, flex: 1.5 },
{
field: 'totalKilos',
headerName: 'Total Kilos',
type: 'number',
width: 150,
align: 'right',
headerAlign: 'right',
valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '',
flex: 0.8
},
{
field: 'cantidadBobinas',
headerName: 'Cant. Bobinas',
type: 'number',
width: 150,
align: 'right',
headerAlign: 'right',
valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '',
flex: 0.8
},
];
const rows = useMemo(() => reportData, [reportData]);
const totalGeneralKilos = useMemo(() =>
reportData.reduce((sum, item) => sum + item.totalKilos, 0),
[reportData]);
const totalGeneralBobinas = useMemo(() =>
reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0),
[reportData]);
// eslint-disable-next-line react/display-name
const CustomFooter = () => {
// Podrías añadir una condición para no renderizar si no hay datos/totales
// if (totalGeneralKilos === 0 && totalGeneralBobinas === 0) return null;
return (
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between', // Separa la paginación (izquierda) de los totales (derecha)
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px', // Altura estándar para el footer
// No es necesario p: aquí si los hijos lo manejan o el GridFooterContainer lo aplica por defecto
}}>
{/* Box para la paginación estándar */}
<Box sx={{
display: 'flex',
alignItems: 'center',
flexShrink: 0, // Evita que este box se encoja si los totales son anchos
overflow: 'hidden', // Para asegurar que no desborde si el contenido interno es muy ancho
px: 1, // Padding horizontal para el contenedor de la paginación
// Considera un flexGrow o un width/maxWidth si necesitas más control sobre el espacio de la paginación
// Ejemplo: flexGrow: 1, maxWidth: 'calc(100% - 250px)' (para dejar espacio a los totales)
}}>
<GridFooter
sx={{
borderTop: 'none', // Quitar el borde superior del GridFooter interno
width: 'auto', // Permite que el GridFooter se ajuste a su contenido (paginador)
'& .MuiToolbar-root': { // Ajustar padding del toolbar de paginación
paddingLeft: 0, // O un valor pequeño si es necesario
paddingRight: 0,
},
// Mantenemos oculto el contador de filas seleccionadas si no lo queremos
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
{/* Box para los totales personalizados */}
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
whiteSpace: 'nowrap', // Evita que los totales hagan salto de línea
overflowX: 'auto', // Scroll DENTRO de este Box si los totales son muy anchos
px: 2, // Padding horizontal para el contenedor de los totales (ajusta pr:2 de tu ejemplo)
flexShrink: 1, // Permitir que este contenedor se encoja si la paginación necesita más espacio
}}>
<Typography variant="subtitle2" sx={{ mr: 1, fontWeight: 'bold' }}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={{ mr: 2, minWidth: '80px', textAlign: 'right', fontWeight: 'bold' }}>
Kg: {totalGeneralKilos.toLocaleString('es-AR')}
</Typography>
<Typography variant="subtitle2" sx={{ minWidth: '80px', textAlign: 'right', fontWeight: 'bold' }}>
Cant: {totalGeneralBobinas.toLocaleString('es-AR')}
</Typography>
</Box>
</GridFooterContainer>
);
};
if (showParamSelector) {
return (
@@ -171,55 +258,33 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>Planta</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Publicación</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Total Kilos</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Cant. Bobinas</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(groupedData).map(([plantaKey, publicaciones]) => {
const totalKilosPlanta = publicaciones.reduce((sum, item) => sum + item.totalKilos, 0);
const totalBobinasPlanta = publicaciones.reduce((sum, item) => sum + item.cantidadBobinas, 0);
return (
<React.Fragment key={plantaKey}>
<TableRow sx={{ backgroundColor: 'rgba(0, 0, 0, 0.08)' }}>
<TableCell colSpan={4} sx={{ fontWeight: 'bold' }}>{plantaKey}</TableCell>
</TableRow>
{publicaciones.map((pub, pubIdx) => (
<TableRow key={`${plantaKey}-${pubIdx}`}>
<TableCell></TableCell> {/* Columna Planta vacía para esta fila */}
<TableCell>{pub.nombrePublicacion}</TableCell>
<TableCell align="right">{pub.totalKilos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{pub.cantidadBobinas.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
{/* Fila de totales por planta */}
<TableRow sx={{ backgroundColor: 'rgba(0, 0, 0, 0.04)'}}>
<TableCell colSpan={2} align="right" sx={{ fontWeight: 'bold', fontStyle:'italic' }}>Total Planta ({plantaKey}):</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', fontStyle:'italic' }}>{totalKilosPlanta.toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', fontStyle:'italic' }}>{totalBobinasPlanta.toLocaleString('es-AR')}</TableCell>
</TableRow>
</React.Fragment>
)})}
</TableBody>
<TableFooter>
<TableRow sx={{backgroundColor: 'grey.300'}}>
<TableCell colSpan={2} align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>TOTAL GENERAL:</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{totalGeneralKilos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{totalGeneralBobinas.toLocaleString('es-AR')}</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
<Paper sx={{
height: 'calc(100vh - 380px)', // Ajustar altura para dar espacio al footer
width: '100%',
mt: 2,
'& .MuiDataGrid-footerContainer': { // Asegurar que el contenedor del footer tenga suficiente espacio
minHeight: '52px', // o el alto que necesite tu CustomFooter
}
}}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
slots={{ footer: CustomFooter }}
density="compact"
// pageSizeOptions={[10, 25, 50]} // Descomentar si deseas que el usuario cambie el tamaño de página
// initialState={{
// pagination: {
// paginationModel: { pageSize: 25, page: 0 },
// },
// }}
// autoHeight // Si se usa autoHeight, el `height` del Paper no aplicará scroll a la tabla
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>

View File

@@ -0,0 +1,388 @@
import React, { useState, useCallback, useMemo } from 'react';
import { Box, Typography, Paper, CircularProgress, Alert, Button, Divider, type SxProps, type Theme } from '@mui/material';
// No necesitaremos DataGrid para la estructura principal, solo para el detalle si se decide mostrar.
import reportesService from '../../services/Reportes/reportesService';
import type { ControlDevolucionesDataResponseDto } from '../../models/dtos/Reportes/ControlDevolucionesDataResponseDto';
import SeleccionaReporteControlDevoluciones from './SeleccionaReporteControlDevoluciones';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteControlDevolucionesPage: React.FC = () => {
// ... (estados y funciones de manejo de datos sin cambios significativos, excepto cómo se renderiza) ...
const [reportData, setReportData] = useState<ControlDevolucionesDataResponseDto | 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<{ fecha: string; idEmpresa: number; nombreEmpresa?: string } | null>(null);
const numberLocaleFormatter = (value: number | null | undefined, showSign = false): string => {
if (value == null) return '';
const formatted = Number(value).toLocaleString('es-AR');
if (showSign && value > 0 && value !== 0) return `+${formatted}`;
return formatted;
};
const handleGenerarReporte = useCallback(async (params: { fecha: string; idEmpresa: number }) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setReportData(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
try {
const data = await reportesService.getControlDevolucionesData(params);
setReportData(data);
if (data.detallesCtrlDevoluciones.length === 0 &&
data.devolucionesOtrosDias.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);
} finally {
setLoading(false);
}
}, []);
const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true);
setReportData(null);
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
}, []);
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.getControlDevolucionesPdf(currentParams);
if (blob.type === "application/json") { // Error devuelto como 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("Permita popups para ver el PDF.")
}
} catch (err) {
setError('Ocurrió un error al generar el PDF.');
} finally {
setLoadingPdf(false);
}
}, [currentParams]);
// Cálculos para la visualización estilo PDF
const calculatedValues = useMemo(() => {
// Comprobación más robusta al inicio
if (!reportData || !reportData.detallesCtrlDevoluciones || reportData.detallesCtrlDevoluciones.length === 0) {
// Si no hay detalles, pero sí hay devoluciones de otros días
if (reportData?.devolucionesOtrosDias && reportData.devolucionesOtrosDias.length > 0) {
const totalDevolucionOtrosDias = reportData.devolucionesOtrosDias.reduce((sum, item) => sum + item.devueltos, 0);
return {
ingresadosPorRemito: 0,
cantidadCanillas: 0,
llevadosAcc: 0, devueltosAcc: 0, totalAcc: 0,
llevadosCan: 0, devueltosCan: 0, totalCan: 0,
// sumLlevadosTotal: 0, // No es necesario devolverlos si no se usan directamente en UI
// sumDevueltosTotal: 0,
totalDevolucionFecha: 0,
totalDevolucionOtrosDias,
totalDevolucion: totalDevolucionOtrosDias,
sinCargo: 0,
sobrantes: 0,
diferencia: totalDevolucionOtrosDias, // O 0 si se prefiere una diferencia nula
};
}
return null; // Si no hay ningún dato relevante
}
const detalles = reportData.detallesCtrlDevoluciones;
const primerDetalle = detalles[0];
const ingresadosPorRemitoRDLC = primerDetalle.ingresados;
const sobrantesRDLC = primerDetalle.sobrantes;
const sinCargoRDLC = primerDetalle.sinCargo;
const cantidadCanillas = primerDetalle.totalNoAccionistas;
const sumLlevadosTotalRDLC = detalles.reduce((sum, item) => sum + item.llevados, 0);
const sumDevueltosTotalRDLC = detalles.reduce((sum, item) => sum + item.devueltos, 0);
const accionistasDetalle = detalles.find(d => d.tipo?.toLowerCase() === 'accionistas');
const llevadosAcc = accionistasDetalle?.llevados || 0;
const devueltosAcc = accionistasDetalle?.devueltos || 0;
const totalAcc = llevadosAcc - devueltosAcc;
const canillitasDetalle = detalles.find(d => d.tipo?.toLowerCase() === 'canillitas');
const llevadosCan = canillitasDetalle?.llevados || 0;
const devueltosCan = canillitasDetalle?.devueltos || 0;
const totalCan = llevadosCan - devueltosCan;
const totalDevolucionFechaRDLC = ingresadosPorRemitoRDLC - sumLlevadosTotalRDLC + sumDevueltosTotalRDLC;
// Usar encadenamiento opcional y valor por defecto para devolucionesOtrosDias
const totalDevolucionOtrosDiasRDLC = reportData.devolucionesOtrosDias?.reduce((sum, item) => sum + item.devueltos, 0) || 0;
const totalDevolucionRDLC = totalDevolucionFechaRDLC + totalDevolucionOtrosDiasRDLC;
// Fórmula de diferencia del RDLC
const diferencia = ingresadosPorRemitoRDLC - sumLlevadosTotalRDLC + sumDevueltosTotalRDLC - sobrantesRDLC - sinCargoRDLC;
return {
ingresadosPorRemito: ingresadosPorRemitoRDLC,
cantidadCanillas,
llevadosAcc, devueltosAcc, totalAcc,
llevadosCan, devueltosCan, totalCan,
totalDevolucionFecha: totalDevolucionFechaRDLC,
totalDevolucionOtrosDias: totalDevolucionOtrosDiasRDLC,
totalDevolucion: totalDevolucionRDLC,
sinCargo: sinCargoRDLC,
sobrantes: sobrantesRDLC,
diferencia, // Usar la variable 'diferencia' que contiene el cálculo correcto
};
}, [reportData]);
const handleExportToExcel = useCallback(() => {
if (!reportData || !calculatedValues || !currentParams) {
alert("No hay datos para exportar.");
return;
}
const dataForExcel: any[][] = [];
// --- Títulos y Cabecera ---
dataForExcel.push(["Control de Devoluciones"]); // Título Principal
dataForExcel.push(["Canillas / Accionistas"]); // Subtítulo
dataForExcel.push([]); // Fila vacía para espaciado
dataForExcel.push([
`Fecha Consultada: ${currentParams.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''}`,
"", // Celda vacía para espaciado o para alinear con la segunda columna si fuera necesario
`Cantidad Canillas: ${calculatedValues.cantidadCanillas}`
]);
dataForExcel.push([]);
dataForExcel.push([currentParams.nombreEmpresa || 'EL DIA']); // Nombre de la Empresa/Publicación
dataForExcel.push([]);
// --- Cuerpo del Reporte ---
dataForExcel.push(["Ingresados por Remito:", calculatedValues.ingresadosPorRemito]);
dataForExcel.push(["----------------------------------", "-------------------"]); // Línea divisoria (estilo simple)
dataForExcel.push(["Accionistas"]);
dataForExcel.push(["Llevados", -calculatedValues.llevadosAcc]);
dataForExcel.push(["Devueltos", calculatedValues.devueltosAcc]);
dataForExcel.push(["Total", -calculatedValues.totalAcc]); // Fila de Total con estilo
dataForExcel.push([]);
dataForExcel.push(["Canillitas"]);
dataForExcel.push(["Llevados", -calculatedValues.llevadosCan]);
dataForExcel.push(["Devueltos", calculatedValues.devueltosCan]);
dataForExcel.push(["Total", -calculatedValues.totalCan]); // Fila de Total con estilo
dataForExcel.push(["==================================", "==================="]); // Línea divisoria sólida
dataForExcel.push(["Total Devolución a la Fecha", calculatedValues.totalDevolucionFecha]);
dataForExcel.push(["Total Devolución Días Anteriores", calculatedValues.totalDevolucionOtrosDias]);
dataForExcel.push(["Total Devolución", calculatedValues.totalDevolucion]); // Fila de Total con estilo
dataForExcel.push(["----------------------------------", "-------------------"]);
dataForExcel.push(["Sin Cargo", calculatedValues.sinCargo]);
dataForExcel.push(["Sobrantes", -calculatedValues.sobrantes]);
dataForExcel.push(["Diferencia", calculatedValues.diferencia]); // Fila de Total con estilo
// --- Crear Hoja y Libro ---
const ws = XLSX.utils.aoa_to_sheet(dataForExcel);
// Ajustar anchos de columna (opcional, pero recomendado)
// Esto es un cálculo aproximado, puedes ajustarlo
const colWidths = [
{ wch: 40 }, // Columna A (Etiquetas)
{ wch: 15 }, // Columna B (Valores)
{ wch: 25 } // Columna C (para Cantidad Canillas)
];
ws['!cols'] = colWidths;
// Fusionar celdas para títulos (opcional, requiere más trabajo con la estructura de 'ws')
// Ejemplo para el título principal (ocuparía A1:C1)
if (!ws['!merges']) ws['!merges'] = [];
ws['!merges'].push({ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }); // Fusionar A1 a C1
ws['!merges'].push({ s: { r: 1, c: 0 }, e: { r: 1, c: 2 } }); // Fusionar A2 a C2
ws['!merges'].push({ s: { r: 5, c: 0 }, e: { r: 5, c: 2 } }); // Fusionar celda de Nombre Empresa
// Aplicar formato numérico (esto es más avanzado y depende de cómo quieras los números en Excel)
// Por ahora, los números se exportarán como números si son de tipo number en dataForExcel.
// Para formato de moneda o miles, tendrías que modificar las celdas en el objeto 'ws'
// o asegurarte de que los valores en 'dataForExcel' ya estén como strings formateados si Excel no los interpreta bien.
// Por simplicidad, los dejamos como números y Excel usará su formato por defecto.
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "ControlDevoluciones");
let fileName = "ReporteControlDevoluciones";
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
fileName += `_${currentParams.fecha}`;
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
}, [reportData, calculatedValues, currentParams]);
// Componente para una fila del reporte usando Box con Flexbox
interface ReportRowProps {
label: string;
value: string | number;
boldValue?: boolean;
valueAlign?: 'flex-start' | 'flex-end' | 'center'; // Para justifyContent del valor
sx?: SxProps<Theme>;
isTotalLine?: boolean; // Para la línea superior de los totales de Accionistas/Canillitas
}
const ReportRow: React.FC<ReportRowProps> =
({ label, value, boldValue = false, valueAlign = 'flex-end', sx, isTotalLine = false }) => (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 0.3, // Padding vertical más ajustado
...(isTotalLine && { borderTop: '2px solid black', pt: 0.8, mt: 0.2 }),
...sx,
}}
>
<Typography variant="body2" sx={{ flexGrow: 1, pr: 1 /* Espacio antes del valor */ }}>{label}</Typography>
<Typography
variant="body2"
sx={{
fontWeight: boldValue ? 'bold' : 'normal',
textAlign: valueAlign === 'flex-start' ? 'left' : valueAlign === 'flex-end' ? 'right' : 'center',
minWidth: '80px', // Para asegurar algo de espacio para el valor
}}
>
{typeof value === 'number' ? numberLocaleFormatter(value) : value}
</Typography>
</Box>
);
// Componente para subcabeceras (como "Ingresados por Remito:")
interface ReportSubHeaderProps {
label: string;
value: string | number;
sx?: SxProps<Theme>;
}
const ReportSubHeader: React.FC<ReportSubHeaderProps> = ({ label, value, sx }) => (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
py: 0.5,
...sx,
}}
>
<Typography variant="body2" sx={{ fontWeight: 'bold', flexGrow: 1, pr: 1 }}>{label}</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', textAlign: 'right', minWidth: '80px' }}>
{numberLocaleFormatter(Number(value))}
</Typography>
</Box>
);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteControlDevoluciones
onGenerarReporte={handleGenerarReporte}
isLoading={loading}
apiErrorMessage={apiErrorParams}
/>
</Paper>
</Box>
);
}
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', my: 5 }}><CircularProgress /></Box>;
if (error && !reportData) return <Alert severity="info" sx={{ m: 2 }}>{error}</Alert>;
if (!reportData || !calculatedValues || !currentParams) return <Typography sx={{ m: 2, fontStyle: 'italic' }}>Seleccione parámetros para generar el reporte.</Typography>;
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Control de Devoluciones</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<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>
<Typography variant="subtitle1" gutterBottom sx={{ textAlign: 'center', fontWeight: 'bold' }}>
Canillas / Accionistas
</Typography>
<Paper sx={{ p: 2, maxWidth: 450, mx: 'auto' }}> {/* Ancho ligeramente reducido */}
{/* Cabecera con Fecha y Cantidad Canillas */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2">Fecha Consultada: <strong>{currentParams.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}</strong></Typography>
<Typography variant="body2">Cantidad Canillas: <strong>{calculatedValues.cantidadCanillas}</strong></Typography>
</Box>
<Typography variant="subtitle1" align="center" sx={{ my: 1, fontWeight: 'bold' }}>
{currentParams.nombreEmpresa || 'EL DIA'}
</Typography>
<ReportSubHeader label="Ingresados por Remito:" value={calculatedValues.ingresadosPorRemito} />
<Divider sx={{ my: 1, borderStyle: 'dashed', borderColor: 'grey.500' }} />
<Typography variant="body2" sx={{ fontWeight: 'bold', mt: 1 }}>Accionistas</Typography>
<ReportRow label="Llevados" value={-calculatedValues.llevadosAcc} />
<ReportRow label="Devueltos" value={calculatedValues.devueltosAcc} />
<ReportRow label="Total" value={-calculatedValues.totalAcc} boldValue isTotalLine />
<Typography variant="body2" sx={{ fontWeight: 'bold', mt: 1.5 }}>Canillitas</Typography>
<ReportRow label="Llevados" value={-calculatedValues.llevadosCan} />
<ReportRow label="Devueltos" value={calculatedValues.devueltosCan} />
<ReportRow label="Total" value={-calculatedValues.totalCan} boldValue isTotalLine />
<Divider sx={{ my: 1, borderColor: 'grey.500' }} />
<ReportRow label="Total Devolución a la Fecha" value={calculatedValues.totalDevolucionFecha} boldValue />
<ReportRow label="Total Devolución Días Anteriores" value={calculatedValues.totalDevolucionOtrosDias} boldValue />
<Box sx={{ borderTop: '1px solid black', pt: 0.5, mt: 0.5 }}> {/* Línea sólida para Total Devolución */}
<ReportRow label="Total Devolución" value={calculatedValues.totalDevolucion} boldValue />
</Box>
<Divider sx={{ my: 1, borderStyle: 'dashed', borderColor: 'grey.500' }} />
<ReportRow label="Sin Cargo" value={calculatedValues.sinCargo} boldValue />
<ReportRow label="Sobrantes" value={-calculatedValues.sobrantes} boldValue />
<Box sx={{ borderTop: '1px solid black', pt: 0.5, mt: 0.5 }}> {/* Línea sólida para Diferencia */}
<ReportRow label="Diferencia" value={calculatedValues.diferencia} boldValue />
</Box>
</Paper>
</Box>
);
};
export default ReporteControlDevolucionesPage;

View File

@@ -1,8 +1,9 @@
import React, { useState, useCallback } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto';
import type { BalanceCuentaDistDto } from '../../models/dtos/Reportes/BalanceCuentaDistDto';
@@ -12,22 +13,214 @@ import SeleccionaReporteCuentasDistribuidores from './SeleccionaReporteCuentasDi
import * as XLSX from 'xlsx';
import axios from 'axios';
type MovimientoConSaldo = BalanceCuentaDistDto & { id: string; saldoAcumulado: number };
type NotaConSaldo = BalanceCuentaDebCredDto & { id: string; saldoAcumulado: number };
type PagoConSaldo = BalanceCuentaPagosDto & { id: string; saldoAcumulado: number };
const ReporteCuentasDistribuidoresPage: React.FC = () => {
const [reportData, setReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
const [originalReportData, setOriginalReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
const [movimientosConSaldo, setMovimientosConSaldo] = useState<MovimientoConSaldo[]>([]);
const [notasConSaldo, setNotasConSaldo] = useState<NotaConSaldo[]>([]);
const [pagosConSaldo, setPagosConSaldo] = useState<PagoConSaldo[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [loadingPdf, setLoadingPdf] = useState<boolean>(false);
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
const [showParamSelector, setShowParamSelector] = useState(true);
const [showParamSelector, setShowParamSelector] = useState<boolean>(true);
const [currentParams, setCurrentParams] = useState<{
idDistribuidor: number;
idEmpresa: number;
fechaDesde: string;
fechaHasta: string;
nombreDistribuidor?: string; // Para el PDF y nombre de archivo
nombreEmpresa?: string; // Para el PDF y nombre de archivo
nombreDistribuidor?: string;
nombreEmpresa?: string;
} | null>(null);
// Calcula saldos acumulados seccion por seccion
const calcularSaldosPorSeccion = (data: ReporteCuentasDistribuidorResponseDto) => {
const procesarLista = <T extends { fecha: string | Date; debe?: number; haber?: number; id?: number | string }>(
lista: T[],
idPrefix: string,
saldoInicial: number
): Array<T & { id: string; saldoAcumulado: number }> => {
let acumulado = saldoInicial;
return lista
.slice()
.sort((a, b) => new Date(a.fecha).getTime() - new Date(b.fecha).getTime())
.map((item, idx) => {
const debe = Number(item.debe) || 0;
const haber = Number(item.haber) || 0;
acumulado += debe - haber;
return {
...item,
id: item.id?.toString() || `${idPrefix}-${idx}`,
saldoAcumulado: acumulado
};
});
};
const movs = procesarLista(data.entradasSalidas, 'mov', 0);
const ultimoMov = movs.length ? movs[movs.length - 1].saldoAcumulado : 0;
const notas = procesarLista(data.debitosCreditos, 'nota', ultimoMov);
const ultimoNota = notas.length ? notas[notas.length - 1].saldoAcumulado : ultimoMov;
const pagos = procesarLista(data.pagos, 'pago', ultimoNota);
return { movs, notas, pagos };
};
// Genera columnas con formateo
const generarColumns = (): { mov: GridColDef[]; notas: GridColDef[]; pagos: GridColDef[] } => ({
mov: [
{ field: 'fecha', headerName: 'Fecha', width: 100 },
{ field: 'publicacion', headerName: 'Publicación', flex: 1 },
{ field: 'remito', headerName: 'Remito', width: 100 },
{
field: 'debe',
headerName: 'Debe',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'debe', headerName: 'Debe', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
{
field: 'haber',
headerName: 'Haber',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'haber', headerName: 'Haber', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
{
field: 'saldoAcumulado',
headerName: 'Saldo',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'saldoAcumulado', headerName: 'Saldo', type: 'number', width: 150, align: 'right', headerAlign: 'right' }
],
notas: [
{ field: 'fecha', headerName: 'Fecha', width: 100 },
{ field: 'referencia', headerName: 'Referencia', flex: 1 },
{
field: 'debe',
headerName: 'Debe',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'debe', headerName: 'Debe', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
{
field: 'haber',
headerName: 'Haber',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'haber', headerName: 'Haber', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
{
field: 'saldoAcumulado',
headerName: 'Saldo',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'saldoAcumulado', headerName: 'Saldo', type: 'number', width: 150, align: 'right', headerAlign: 'right' }
],
pagos: [
{ field: 'fecha', headerName: 'Fecha', width: 100 },
{ field: 'recibo', headerName: 'Recibo', width: 100 },
{ field: 'tipo', headerName: 'Tipo', width: 150 },
{ field: 'detalle', headerName: 'Detalle', flex: 1 },
{
field: 'debe',
headerName: 'Debe',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'debe', headerName: 'Debe', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
{
field: 'haber',
headerName: 'Haber',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'haber', headerName: 'Haber', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
{
field: 'saldoAcumulado',
headerName: 'Saldo',
type: 'number',
width: 130,
align: 'right',
headerAlign: 'right',
valueFormatter: (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
},
//{ field: 'saldoAcumulado', headerName: 'Saldo', type: 'number', width: 150, align: 'right', headerAlign: 'right' }
]
});
// Renderiza DataGrid con footer de totales
const renderDataGrid = (
rows: Array<{ debe?: number; haber?: number }>,
columns: GridColDef[]
) => {
const totalDebe = rows.reduce((sum, r) => sum + (r.debe || 0), 0);
const totalHaber = rows.reduce((sum, r) => sum + (r.haber || 0), 0);
return rows.length ? (
<Paper sx={{ height: 350, width: '100%', mb: 2 }}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{
footer: () => (
<GridFooterContainer>
<GridFooter sx={{ borderTop: 'none' }} />
<Box sx={{ p: 1, fontWeight: 'bold' }}>
<Typography variant="subtitle2" component="span" sx={{ mr: 2 }}>
TOTA DEBE: <strong>{totalDebe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
</Typography>
<Typography variant="subtitle2" component="span">
TOTAL HABER: <strong>{totalHaber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
</Typography>
</Box>
</GridFooterContainer>
)
}}
/>
</Paper>
) : (
<Typography sx={{ fontStyle: 'italic', mb: 2 }}>No hay registros</Typography>
);
};
const handleGenerarReporte = useCallback(async (params: {
idDistribuidor: number;
idEmpresa: number;
@@ -35,16 +228,18 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
fechaHasta: string;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setReportData(null);
// Obtener nombres para el PDF/Excel
const distService = (await import('../../services/Distribucion/distribuidorService')).default;
const distData = await distService.getDistribuidorById(params.idDistribuidor);
const empService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empService.getEmpresaById(params.idEmpresa);
setOriginalReportData(null);
setMovimientosConSaldo([]);
setNotasConSaldo([]);
setPagosConSaldo([]);
const distSvc = (await import('../../services/Distribucion/distribuidorService')).default;
const empSvc = (await import('../../services/Distribucion/empresaService')).default;
const [distData, empData] = await Promise.all([
distSvc.getDistribuidorById(params.idDistribuidor),
empSvc.getEmpresaById(params.idEmpresa)
]);
setCurrentParams({
...params,
nombreDistribuidor: distData?.nombre,
@@ -53,17 +248,17 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
try {
const data = await reportesService.getReporteCuentasDistribuidor(params);
setReportData(data);
const noData = (!data.entradasSalidas?.length && !data.debitosCreditos?.length && !data.pagos?.length && !data.saldos?.length);
if (noData) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setOriginalReportData(data);
const { movs, notas, pagos } = calcularSaldosPorSeccion(data);
setMovimientosConSaldo(movs);
setNotasConSaldo(notas);
setPagosConSaldo(pagos);
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);
} catch (error: any) {
const msg = axios.isAxiosError(error) && error.response?.data?.message
? error.response.data.message
: 'Error al generar el reporte';
setApiErrorParams(msg);
} finally {
setLoading(false);
}
@@ -71,166 +266,46 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true);
setReportData(null);
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
setOriginalReportData(null);
setMovimientosConSaldo([]);
setNotasConSaldo([]);
setPagosConSaldo([]);
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData) {
alert("No hay datos para exportar.");
return;
}
if (!originalReportData) return;
const wb = XLSX.utils.book_new();
if (reportData.saldos?.length) {
const saldosToExport = reportData.saldos.map(item => ({ "Saldo Actual": item.monto }));
const wsSaldos = XLSX.utils.json_to_sheet(saldosToExport);
XLSX.utils.book_append_sheet(wb, wsSaldos, "SaldoActual");
if (movimientosConSaldo.length) {
const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
}
if (reportData.entradasSalidas?.length) {
const esToExport = reportData.entradasSalidas.map(item => ({
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }),
"Publicación": item.publicacion, "Remito": item.remito, "Cantidad": item.cantidad,
"Observación": item.observacion, "Debe": item.debe, "Haber": item.haber,
}));
const wsES = XLSX.utils.json_to_sheet(esToExport);
XLSX.utils.book_append_sheet(wb, wsES, "EntradasSalidas");
if (notasConSaldo.length) {
const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
XLSX.utils.book_append_sheet(wb, ws, 'Notas');
}
if (reportData.debitosCreditos?.length) {
const dcToExport = reportData.debitosCreditos.map(item => ({
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }),
"Referencia": item.referencia, "Debe": item.debe, "Haber": item.haber,
}));
const wsDC = XLSX.utils.json_to_sheet(dcToExport);
XLSX.utils.book_append_sheet(wb, wsDC, "DebitosCreditos");
if (pagosConSaldo.length) {
const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
}
if (reportData.pagos?.length) {
const paToExport = reportData.pagos.map(item => ({
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }),
"Recibo": item.recibo, "Tipo": item.tipo, "Debe": item.debe, "Haber": item.haber,
"Detalle": item.detalle,
}));
const wsPA = XLSX.utils.json_to_sheet(paToExport);
XLSX.utils.book_append_sheet(wb, wsPA, "Pagos");
}
let fileName = "ReporteCuentaDistribuidor";
if (currentParams) {
fileName += `_${currentParams.nombreDistribuidor?.replace(/\s+/g, '') ?? `Dist${currentParams.idDistribuidor}`}`;
fileName += `_Emp${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? currentParams.idEmpresa}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
}, [reportData, currentParams]);
XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
}, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
const handleGenerarYAbrirPdf = useCallback(async () => {
if (!currentParams) {
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
return;
}
if (!currentParams) return;
setLoadingPdf(true);
setError(null);
try {
const blob = await reportesService.getReporteCuentasDistribuidorPdf(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.");
}
window.open(URL.createObjectURL(blob), '_blank');
} catch {
setError('Ocurrió un error al generar el PDF.');
/* manejar error */
} finally {
setLoadingPdf(false);
}
}, [currentParams]);
// --- Funciones para renderizar las tablas ---
const renderEntradasSalidasTable = (data: BalanceCuentaDistDto[]) => (
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell>
<TableCell>Remito</TableCell><TableCell align="right">Cantidad</TableCell>
<TableCell>Observación</TableCell>
<TableCell align="right">Debe</TableCell><TableCell align="right">Haber</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((row, idx) => (
<TableRow key={`es-${idx}`}>
<TableCell>{new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' })}</TableCell>
<TableCell>{row.publicacion}</TableCell><TableCell>{row.remito}</TableCell>
<TableCell align="right">{row.cantidad.toLocaleString('es-AR')}</TableCell>
<TableCell>{row.observacion}</TableCell>
<TableCell align="right">{row.debe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
<TableCell align="right">{row.haber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
const renderDebitosCreditosTable = (data: BalanceCuentaDebCredDto[]) => (
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Fecha</TableCell><TableCell>Referencia</TableCell>
<TableCell align="right">Debe</TableCell><TableCell align="right">Haber</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((row, idx) => (
<TableRow key={`dc-${idx}`}>
<TableCell>{new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' })}</TableCell>
<TableCell>{row.referencia}</TableCell>
<TableCell align="right">{row.debe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
<TableCell align="right">{row.haber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
const renderPagosTable = (data: BalanceCuentaPagosDto[]) => (
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Fecha</TableCell><TableCell>Recibo</TableCell><TableCell>Tipo</TableCell>
<TableCell align="right">Debe</TableCell><TableCell align="right">Haber</TableCell>
<TableCell>Detalle</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((row, idx) => (
<TableRow key={`pa-${idx}`}>
<TableCell>{new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' })}</TableCell>
<TableCell>{row.recibo}</TableCell><TableCell>{row.tipo}</TableCell>
<TableCell align="right">{row.debe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
<TableCell align="right">{row.haber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
<TableCell>{row.detalle}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center' }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteCuentasDistribuidores
onGenerarReporte={handleGenerarReporte}
@@ -243,15 +318,23 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
);
}
// Totales de cada sección para resumen final
const totalMov = movimientosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
const totalNot = notasConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
const totalPag = pagosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
const saldoInicial = originalReportData?.saldos?.[0]?.monto || 0;
const cols = generarColumns();
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Cuenta Corriente Distribuidor</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5">Cuenta Corriente Distribuidor</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 onClick={handleGenerarYAbrirPdf} variant="contained" disabled={!originalReportData} size="small">
{loadingPdf ? <CircularProgress size={20} /> : 'Abrir PDF'}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
<Button onClick={handleExportToExcel} variant="outlined" disabled={!originalReportData} size="small">
Exportar a Excel
</Button>
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
@@ -260,33 +343,31 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
<Paper sx={{ p: 2, mb: 2 }}>
<Typography>Distribuidor: <strong>{currentParams?.nombreDistribuidor}</strong></Typography>
<Typography>Empresa: <strong>{currentParams?.nombreEmpresa}</strong></Typography>
<Typography>Período: <strong>{currentParams?.fechaDesde}</strong> al <strong>{currentParams?.fechaHasta}</strong></Typography>
<Typography>Saldo a la Fecha {new Date().toLocaleDateString('es-AR')}: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
</Paper>
{!loading && !error && reportData && (
<>
<Typography variant="h6" gutterBottom>Saldo Actual</Typography>
{reportData.saldos && reportData.saldos.length > 0 ? (
<Typography sx={{ mb: 2 }}>
{reportData.saldos[0].monto.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
</Typography>
) : <Typography sx={{ mb: 2, fontStyle: 'italic' }}>No hay saldo actual disponible.</Typography>}
<Typography variant="h6" sx={{ mt: 2 }}>Movimientos de Entrada / Salida</Typography>
{renderDataGrid(movimientosConSaldo, cols.mov)}
<Typography variant="h6" gutterBottom>Movimientos (Entradas/Salidas)</Typography>
{reportData.entradasSalidas && reportData.entradasSalidas.length > 0 ?
renderEntradasSalidasTable(reportData.entradasSalidas) : <Typography sx={{ fontStyle: 'italic' }}>No hay movimientos de entradas/salidas.</Typography>}
<Typography variant="h6" sx={{ mt: 2 }}>Notas de Crédito / Débito</Typography>
{renderDataGrid(notasConSaldo, cols.notas)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Notas de Débito/Crédito</Typography>
{reportData.debitosCreditos && reportData.debitosCreditos.length > 0 ?
renderDebitosCreditosTable(reportData.debitosCreditos) : <Typography sx={{ fontStyle: 'italic' }}>No hay notas de débito/crédito.</Typography>}
<Typography variant="h6" sx={{ mt: 2 }}>Pagos Recibidos / Realizados</Typography>
{renderDataGrid(pagosConSaldo, cols.pagos)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Pagos</Typography>
{reportData.pagos && reportData.pagos.length > 0 ?
renderPagosTable(reportData.pagos) : <Typography sx={{ fontStyle: 'italic' }}>No hay pagos registrados.</Typography>}
</>
)}
{!loading && !error && (!reportData || (!reportData.entradasSalidas?.length && !reportData.debitosCreditos?.length && !reportData.pagos?.length && !reportData.saldos?.length)) &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
<Paper sx={{ p: 2, mt: 3 }}>
<Typography variant="h6">Resumen Final</Typography>
<Typography>Movimientos (Debe - Haber): {totalMov.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
<Typography>Notas C/D (Debe - Haber): {totalNot.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
<Typography>Pagos (Debe - Haber): {totalPag.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mt: 1 }}>
Saldo Final del Período: {(totalMov + totalNot + totalPag).toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
</Typography>
</Paper>
</Box>
);
};

View File

@@ -1,16 +1,76 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo, type JSXElementConstructor, type HTMLAttributes } from 'react'; // Añadido JSXElementConstructor, HTMLAttributes
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme // Añadido SxProps, Theme
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter, type GridSlotsComponent } from '@mui/x-data-grid'; // Añadido GridSlotsComponent
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
import type { DetalleDistribucionCanillaDto } from '../../models/dtos/Reportes/DetalleDistribucionCanillaDto';
import type { DetalleDistribucionCanillaAllDto } from '../../models/dtos/Reportes/DetalleDistribucionCanillaAllDto';
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Para el tipo del footer en DataGridSectionProps
type FooterPropsOverrides = {}; // Puedes extender esto si tus footers tienen props específicos
type CustomFooterType = JSXElementConstructor<HTMLAttributes<HTMLDivElement> & { sx?: SxProps<Theme> } & FooterPropsOverrides>;
interface TotalesComunes {
totalCantSalida: number;
totalCantEntrada: number;
vendidos: number;
totalRendir: number;
}
interface DataGridSectionProps {
title: string;
data: Array<any>;
columns: GridColDef[];
isLoading?: boolean;
footerComponent?: CustomFooterType; // Tipo más específico para el footer
height?: number | string;
}
const DataGridSection: React.FC<DataGridSectionProps> = ({ title, data, columns, isLoading, footerComponent, height = 300 }) => {
const rows = useMemo(() => data.map((r, i) => ({ ...r, _internalId: i })), [data]);
if (isLoading) {
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
}
if (!rows || rows.length === 0) {
return <Typography sx={{ mt: 1, fontStyle: 'italic', mb:2 }}>No hay datos para {title.toLowerCase()}.</Typography>;
}
const slotsProp: Partial<GridSlotsComponent> = {};
if (footerComponent) {
slotsProp.footer = footerComponent;
}
return (
<>
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2, fontWeight: 'bold' }}>{title}</Typography>
<Paper sx={{ height: footerComponent ? 'auto' : height, width: '100%', mb: 2, '& .MuiDataGrid-footerContainer': { minHeight: footerComponent ? '52px' : undefined} }}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
getRowId={(row) => row.id || row._internalId}
slots={slotsProp} // Usar el objeto slotsProp
hideFooterSelectedRowCount={!!footerComponent}
autoHeight={!!footerComponent}
sx={!footerComponent ? {} : {
'& .MuiTablePagination-root': { display: 'none' },
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Paper>
</>
);
};
const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | null>(null);
const [loading, setLoading] = useState(false);
@@ -23,8 +83,34 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
idEmpresa: number;
nombreEmpresa?: string;
} | null>(null);
const [pdfSoloTotales, setPdfSoloTotales] = useState(false); // Estado para el tipo de PDF
const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistas, setTotalesAccionistas] = useState<TotalesComunes>(initialTotals);
const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState<TotalesComunes>(initialTotals);
const currencyFormatter = (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
const numberFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const calculateAndSetTotals = (dataArray: Array<any> | undefined, setTotalsFunc: React.Dispatch<React.SetStateAction<TotalesComunes>>) => {
if (dataArray && dataArray.length > 0) {
const totals = dataArray.reduce((acc, item) => {
acc.totalCantSalida += Number(item.totalCantSalida) || 0;
acc.totalCantEntrada += Number(item.totalCantEntrada) || 0;
acc.totalRendir += Number(item.totalRendir) || 0;
return acc;
}, { totalCantSalida: 0, totalCantEntrada: 0, totalRendir: 0 });
totals.vendidos = totals.totalCantSalida - totals.totalCantEntrada;
setTotalsFunc(totals);
} else {
setTotalsFunc(initialTotals);
}
};
const handleGenerarReporte = useCallback(async (params: {
fecha: string;
@@ -33,29 +119,49 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
setLoading(true);
setError(null);
setApiErrorParams(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({...params, nombreEmpresa: empData?.nombre});
setReportData(null);
// Resetear totales
setTotalesCanillas(initialTotals);
setTotalesAccionistas(initialTotals);
setTotalesCanillasOtraFecha(initialTotals);
setTotalesAccionistasOtraFecha(initialTotals);
try {
const data = await reportesService.getReporteDistribucionCanillas(params);
setReportData(data);
const noData = (!data.canillas || data.canillas.length === 0) &&
(!data.canillasAccionistas || data.canillasAccionistas.length === 0) &&
(!data.canillasTodos || data.canillasTodos.length === 0) &&
(!data.controlDevolucionesDetalle || data.controlDevolucionesDetalle.length === 0);
if (noData) {
const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
(arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || item.remito || item.devueltos || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
const processedData = {
canillas: addIds(data.canillas, 'can'),
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'),
controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo')
};
setReportData(processedData);
calculateAndSetTotals(processedData.canillas, setTotalesCanillas);
calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas);
calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha);
calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha);
const noDataFound = Object.values(processedData).every(arr => !arr || arr.length === 0);
if (noDataFound) {
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.';
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);
}
@@ -80,13 +186,18 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
if (data && data.length > 0) {
const exportedData = data.map(item => {
const row: Record<string, any> = {};
// Excluir el 'id' generado para DataGrid si existe
const { id, ...itemData } = item;
Object.keys(fields).forEach(key => {
row[fields[key]] = item[key];
if (key === 'fecha' && item[key]) {
row[fields[key]] = new Date(item[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
row[fields[key]] = (itemData as any)[key]; // Usar itemData
if (key === 'fecha' && (itemData as any)[key]) {
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
}
if ((key === 'totalRendir') && item[key] != null) {
row[fields[key]] = parseFloat(item[key]).toFixed(2);
if ((key === 'totalRendir') && (itemData as any)[key] != null) {
row[fields[key]] = parseFloat((itemData as any)[key]).toFixed(2);
}
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
}
});
return row;
@@ -102,17 +213,19 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
}
};
formatAndSheet(reportData.canillas, "Canillitas_Dia", { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
formatAndSheet(reportData.controlDevolucionesDetalle, "CtrlDev_Detalle", { ingresados: "Ingresados", sobrantes: "Sobrantes", sinCargo: "Sin Cargo", publicacion: "Publicación", llevados: "Llevados", devueltos: "Devueltos", tipo: "Tipo" });
formatAndSheet(reportData.controlDevolucionesRemitos, "CtrlDev_Remitos", { remito: "Remito Ingresado" });
formatAndSheet(reportData.controlDevolucionesOtrosDias, "CtrlDev_OtrosDias", { devueltos: "Devueltos Otros Días" });
// Definición de campos para la exportación
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos);
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
let fileName = "ReporteDistribucionCanillitas";
let fileName = "ReporteDetalleDistribucionCanillitas";
if (currentParams) {
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
fileName += `_${currentParams.fecha}`;
@@ -128,11 +241,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
}
setLoadingPdf(true);
setError(null);
setPdfSoloTotales(soloTotales); // Guardar la opción para el nombre del archivo
setPdfSoloTotales(soloTotales);
try {
const blob = await reportesService.getReporteDistribucionCanillasPdf({
...currentParams,
soloTotales // Pasar el parámetro al servicio
soloTotales
});
if (blob.type === "application/json") {
const text = await blob.text();
@@ -150,53 +263,93 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
}
}, [currentParams]);
const renderTableData = (data: DetalleDistribucionCanillaDto[] | DetalleDistribucionCanillaAllDto[], title: string, isAllDto: boolean = false) => {
if (!data || data.length === 0) return <Typography sx={{mt:1, fontStyle:'italic'}}>No hay datos para {title.toLowerCase()}.</Typography>;
return (
<>
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2, fontWeight:'bold' }}>{title}</Typography>
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Publicación</TableCell>
<TableCell>{isAllDto ? 'Tipo Vendedor' : 'Canillita'}</TableCell>
{ (data[0] as DetalleDistribucionCanillaDto).fecha && <TableCell>Fecha Mov.</TableCell> }
<TableCell align="right">Llevados</TableCell>
<TableCell align="right">Devueltos</TableCell>
<TableCell align="right">Vendidos</TableCell>
<TableCell align="right">A Rendir</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((row, idx) => {
const item = row as any; // Para acceso dinámico
const fechaMov = item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : null;
return (
<TableRow key={`${title.replace(/\s+/g, '')}-${idx}`}>
<TableCell>{item.publicacion}</TableCell>
<TableCell>{isAllDto ? item.tipoVendedor : item.canilla}</TableCell>
{ fechaMov && <TableCell>{fechaMov}</TableCell> }
<TableCell align="right">{item.totalCantSalida.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{item.totalCantEntrada.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{(item.totalCantSalida - item.totalCantEntrada).toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{item.totalRendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
</TableRow>
)})}
</TableBody>
</Table>
</TableContainer>
</>
// --- Definiciones de Columnas ---
const commonColumns: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const commonColumnsWithFecha: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 },
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const columnsTodos: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
// --- Custom Footers ---
const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno
const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => {
if (!colConfig) return { width: 100, textAlign: 'right' as const, pr: isPlaceholder ? 0 : 1, fontWeight: 'bold' };
const defaultWidth = colConfig.field === 'publicacion' ? 200 : (colConfig.field === 'canilla' || colConfig.field === 'tipoVendedor' ? 150 : 100);
return {
width: colConfig.width || defaultWidth,
flex: colConfig.flex || undefined,
minWidth: colConfig.minWidth || colConfig.width || defaultWidth,
textAlign: (colConfig.align || 'right') as 'right' | 'left' | 'center',
pr: isPlaceholder || colConfig.field === columnsDef[columnsDef.length-1].field ? 0 : 1,
fontWeight: 'bold',
};
};
// eslint-disable-next-line react/display-name
const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props
<GridFooterContainer {...props} sx={{ // Pasar props y combinar sx
...(props.sx as any), // Castear props.sx temporalmente si es necesario
justifyContent: 'space-between', alignItems: 'center', width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
}}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<GridFooter sx={{ borderTop: 'none', '& .MuiTablePagination-root, & .MuiDataGrid-selectedRowCount': { display: 'none' }}} />
</Box>
<Box sx={{
p: theme => theme.spacing(0, 1), display: 'flex', alignItems: 'center',
fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto',
}}>
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'publicacion' || c.field === columnsDef[0].field)), textAlign:'right' }}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'canilla' || c.field === 'tipoVendedor' || c.field === columnsDef[1].field), true) }}></Typography>
{columnsDef.some(c => c.field === 'fecha') &&
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'fecha'), true) }}></Typography>
}
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'totalCantSalida'))}>{numberFormatter(totals.totalCantSalida)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'totalCantEntrada'))}>{numberFormatter(totals.totalCantEntrada)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'vendidos'))}>{numberFormatter(totals.vendidos)}</Typography>
<Typography variant="subtitle2" sx={{...getCellStyle(columnsDef.find(c => c.field === 'totalRendir')), pr:0 }}>{currencyFormatter(totals.totalRendir)}</Typography>
</Box>
</GridFooterContainer>
);
return FooterComponent;
};
const FooterCanillas = useMemo(() => createCustomFooterComponent(totalesCanillas, commonColumns), [totalesCanillas]);
const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]);
const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
const FooterAccionistasOtraFecha = useMemo(() => createCustomFooterComponent(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteDetalleDistribucionCanillas
onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros}
onCancel={handleVolverAParametros} // Aunque el componente no lo use directamente.
isLoading={loading}
apiErrorMessage={apiErrorParams}
/>
@@ -204,11 +357,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</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: Detalle Distribución Canillitas</Typography>
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''}</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
@@ -225,22 +378,27 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && (
<>
{renderTableData(reportData.canillas, "Canillitas")}
{renderTableData(reportData.canillasAccionistas, "Accionistas")}
{renderTableData(reportData.canillasTodos, "Resumen por Tipo de Vendedor", true)}
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
<DataGridSection title="Resumen por Tipo de Vendedor" data={reportData.canillasTodos || []} columns={columnsTodos} height={220}/>
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
renderTableData(reportData.canillasLiquidadasOtraFecha, "Canillitas (Liquidados de Otras Fechas)")}
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
renderTableData(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas (Liquidados de Otras Fechas)")}
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
</>
)}
{!loading && !error && (!reportData || ((!reportData.canillas || reportData.canillas.length === 0) && (!reportData.canillasAccionistas || reportData.canillasAccionistas.length === 0))) &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
{!loading && !error && reportData &&
Object.values(reportData).every(arr => !arr || arr.length === 0) &&
<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>
}
</Box>
);
};

View File

@@ -1,8 +1,10 @@
import React, { useState, useCallback } from 'react';
// src/pages/Reportes/ReporteListadoDistribucionCanillasImportePage.tsx
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionCanillasImporteDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasImporteDto';
import SeleccionaReporteListadoDistribucionCanillasImporte from './SeleccionaReporteListadoDistribucionCanillasImporte';
@@ -37,11 +39,13 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
const pubData = await pubService.getPublicacionById(params.idPublicacion);
setCurrentParams({...params, nombrePublicacion: pubData?.nombre});
setCurrentParams({...params, nombrePublicacion: pubData?.nombre}); // Acceder a publicacion.nombre
try {
const data = await reportesService.getListadoDistribucionCanillasImporte(params);
setReportData(data);
if (data.length === 0) {
// DataGrid necesita un 'id' único por fila. La fecha puede no ser suficiente si hay múltiples registros por fecha (aunque el SP agrupa por fecha)
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.fecha}-${index}` }));
setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -69,23 +73,31 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
alert("No hay datos para exportar.");
return;
}
const dataToExport = reportData.map(item => ({
"Fecha": item.fecha, // Ya viene como string dd/MM/yyyy del SP
"Llevados": item.llevados,
"Devueltos": item.devueltos,
"Vendidos": item.vendidos,
"Imp. Publicación": item.totalRendirPublicacion,
"A Rendir": item.totalRendirGeneral,
const dataToExport = reportData.map(({ ...rest }) => ({ // Excluir 'id'
"Fecha": rest.fecha,
"Llevados": rest.llevados,
"Devueltos": rest.devueltos,
"Vendidos": rest.vendidos,
"Importe Publicación": rest.totalRendirPublicacion,
"A Rendir": rest.totalRendirGeneral,
}));
const totales = {
"Fecha": "TOTALES",
"Llevados": reportData.reduce((sum, r) => sum + r.llevados, 0),
"Devueltos": reportData.reduce((sum, r) => sum + r.devueltos, 0),
"Vendidos": reportData.reduce((sum, r) => sum + r.vendidos, 0),
"Importe Publicación": reportData.reduce((sum, r) => sum + r.totalRendirPublicacion, 0),
"A Rendir": reportData.reduce((sum, r) => sum + r.totalRendirGeneral, 0),
};
dataToExport.push(totales);
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 };
const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => {
const maxLen = Math.max(...dataToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
return { wch: maxLen + 2 };
});
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
@@ -126,6 +138,68 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
}
}, [currentParams]);
const currencyFormatter = (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
const numberFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const columns: GridColDef[] = [
{ field: 'fecha', headerName: 'Fecha', width: 120, flex: 0.7 },
{ field: 'llevados', headerName: 'Llevados', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: numberFormatter, flex:0.6 },
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: numberFormatter, flex:0.6 },
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: numberFormatter, flex:0.6 },
{ field: 'totalRendirPublicacion', headerName: 'Importe Publicación', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: currencyFormatter, flex:1 },
{ field: 'totalRendirGeneral', headerName: 'A Rendir', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: currencyFormatter, flex:1 },
];
const rows = useMemo(() => reportData, [reportData]);
const totalLlevados = useMemo(() => reportData.reduce((sum, item) => sum + item.llevados, 0), [reportData]);
const totalDevueltos = useMemo(() => reportData.reduce((sum, item) => sum + item.devueltos, 0), [reportData]);
const totalVendidos = useMemo(() => reportData.reduce((sum, item) => sum + item.vendidos, 0), [reportData]);
const totalImpPub = useMemo(() => reportData.reduce((sum, item) => sum + item.totalRendirPublicacion, 0), [reportData]);
const totalARendir = useMemo(() => reportData.reduce((sum, item) => sum + item.totalRendirGeneral, 0), [reportData]);
const CustomFooter = () => (
<GridFooterContainer sx={{
display: 'flex', // Usar flexbox directamente en el contenedor principal
justifyContent: 'space-between', // Mantiene la parte izquierda a la izquierda y la derecha a la derecha
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
p: theme => theme.spacing(0, 1), // Padding general para el footer container
}}>
{/* Parte izquierda (donde irían los controles estándar del footer) */}
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 /* Evita que este box se encoja si los totales son anchos */ }}>
<GridFooter sx={{
borderTop: 'none',
}} />
</Box>
{/* Parte derecha (totales) */}
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
// marginLeft: 'auto', // Ya no es necesario si GridFooterContainer es flex y justifyContent: 'space-between'
whiteSpace: 'nowrap',
overflowX: 'auto', // Scroll DENTRO de este Box si los totales son muy anchos
maxWidth: 'calc(100% - 50px)', // Ejemplo: Limitar el ancho máximo para dejar espacio a la izquierda (ajustar el valor 50px según sea necesario)
// Esto es una medida adicional si la parte izquierda tuviera contenido visible que no queremos que se solape.
// Si la parte izquierda realmente no muestra nada, esto podría no ser necesario.
}}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', width: columns[0].width, textAlign: 'right', pr: 1 }}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[1].width, width: columns[1].width, textAlign: 'right', pr: 1 }}>{numberFormatter(totalLlevados)}</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[2].width, width: columns[2].width, textAlign: 'right', pr: 1 }}>{numberFormatter(totalDevueltos)}</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[3].width, width: columns[3].width, textAlign: 'right', pr: 1 }}>{numberFormatter(totalVendidos)}</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[4].width, width: columns[4].width, textAlign: 'right', pr: 1 }}>{currencyFormatter(totalImpPub)}</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[5].width, width: columns[5].width, textAlign: 'right', pr: 0 }}>{currencyFormatter(totalARendir)}</Typography>
</Box>
</GridFooterContainer>
);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -144,7 +218,7 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Distribución Canillitas con Importe</Typography>
<Typography variant="h5">Reporte: Distribución Canillitas con Importe ({currentParams?.esAccionista ? "Accionistas" : "Canillitas"})</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"}
@@ -158,36 +232,32 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Fecha</TableCell>
<TableCell align="right">Llevados</TableCell>
<TableCell align="right">Devueltos</TableCell>
<TableCell align="right">Vendidos</TableCell>
<TableCell align="right">Importe Publicación</TableCell>
<TableCell align="right">A Rendir</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.map((row, idx) => (
<TableRow key={`importe-${idx}`}>
<TableCell>{row.fecha}</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>
<TableCell align="right">{row.totalRendirPublicacion.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
<TableCell align="right">{row.totalRendirGeneral.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Paper sx={{
height: 'calc(100vh - 280px)', // Ajusta esta altura para el footer
width: '100%',
mt: 2,
'& .MuiDataGrid-footerContainer': {
minHeight: '52px', // O el alto que necesite tu CustomFooter
}
}}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
slots={{ footer: CustomFooter }}
density="compact"
// pageSizeOptions={[10, 25, 50]} // Descomentar si deseas paginación
// initialState={{
// pagination: {
// paginationModel: { pageSize: 25, page: 0 },
// },
// }}
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>

View File

@@ -1,16 +1,36 @@
// src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx
import React, { useState, useCallback } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales'; // Importación para localización
import reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto';
import type { ListadoDistribucionCanillasSimpleDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasSimpleDto';
import type { ListadoDistribucionCanillasPromedioDiaDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasPromedioDiaDto';
// Asegúrate que esta ruta es correcta
import SeleccionaReporteListadoDistribucionCanillas from './SeleccionaReporteListadoDistribucionCanillas';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Interfaces extendidas para los datos con campos calculados para el DataGrid
interface DetalleDiarioCanillasExtendido extends ListadoDistribucionCanillasSimpleDto {
id: string;
ventaNeta: number;
promedio: number; // Promedio acumulativo de Venta Neta
porcentajeDevolucion: number;
}
interface PromedioDiaCanillasExtendido extends ListadoDistribucionCanillasPromedioDiaDto {
id: string;
porcentajeDevolucion: number;
}
const ReporteListadoDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionCanillasResponseDto | null>(null);
const [detalleDiarioCalculado, setDetalleDiarioCalculado] = useState<DetalleDiarioCanillasExtendido[]>([]);
const [promediosPorDiaCalculado, setPromediosPorDiaCalculado] = useState<PromedioDiaCanillasExtendido[]>([]);
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -23,6 +43,22 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
nombrePublicacion?: string;
} | null>(null);
const [totalesDetalle, setTotalesDetalle] = useState({
llevados: 0,
devueltos: 0,
ventaNeta: 0,
promedioGeneralVentaNeta: 0,
porcentajeDevolucionGeneral: 0,
});
const [totalesPromedios, setTotalesPromedios] = useState({
cantDias: 0,
promLlevados: 0,
promDevueltos: 0,
promVentas: 0,
porcentajeDevolucionGeneral: 0,
});
const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number;
fechaDesde: string;
@@ -31,24 +67,99 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setReportData(null);
setDetalleDiarioCalculado([]);
setPromediosPorDiaCalculado([]);
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 });
setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0});
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
const pubData = await pubService.getPublicacionById(params.idPublicacion);
setCurrentParams({...params, nombrePublicacion: pubData?.nombre });
setCurrentParams({ ...params, nombrePublicacion: pubData?.nombre }); // Asumiendo que pubData es la tupla
try {
const data = await reportesService.getListadoDistribucionCanillas(params);
setReportData(data);
if ((!data.detalleSimple || data.detalleSimple.length === 0) && (!data.promediosPorDia || data.promediosPorDia.length === 0)) {
let acumuladoVentaNeta = 0;
let diasConActividadDetalle = 0;
let ultimoPromedioDetalle = 0;
const detalleCalculadoLocal = data.detalleSimple.map((item, index) => {
const llevados = item.llevados || 0;
const devueltos = item.devueltos || 0;
const ventaNeta = llevados - devueltos;
if (llevados > 0) {
diasConActividadDetalle++;
acumuladoVentaNeta += ventaNeta;
}
const promedioActual = diasConActividadDetalle > 0 ? acumuladoVentaNeta / diasConActividadDetalle : 0;
ultimoPromedioDetalle = promedioActual;
return {
...item,
id: `simple-can-${index}`, // o simple-dist-${index}
ventaNeta: ventaNeta,
promedio: promedioActual,
porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0, // Esto es % Devolución real
};
});
setDetalleDiarioCalculado(detalleCalculadoLocal);
const totalLlevadosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.llevados || 0), 0);
const totalDevueltosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.devueltos || 0), 0);
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
setTotalesDetalle({
llevados: totalLlevadosDetalle,
devueltos: totalDevueltosDetalle,
ventaNeta: totalVentaNetaDetalle,
promedioGeneralVentaNeta: ultimoPromedioDetalle,
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
});
// --- Cálculos para promedios y sus totales ---
const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => {
const promLlevados = item.promedio_Llevados || 0;
const promVentas = item.promedio_Ventas || 0;
return {
...item,
id: `prom-can-${index}`, // o prom-dist-${index}
// LA COLUMNA EN EL PDF SE LLAMA "% Devolución" PERO PARECE SER "% VENTA"
porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
porcentajeDevolucion: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
};
});
setPromediosPorDiaCalculado(promediosCalculadoLocal);
const totalDiasProm = promediosCalculadoLocal.reduce((sum, item) => sum + (item.cant || 0), 0);
const totalPonderadoLlevados = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
// const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0); // No se usa para el % del PDF
const totalPonderadoVentas = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
setTotalesPromedios({
cantDias: totalDiasProm,
promLlevados: totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0,
promDevueltos: totalDiasProm > 0 ? promediosCalculadoLocal.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0) / promediosCalculadoLocal.length :0, // Promedio simple para mostrar
promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0,
// Para la fila "General" de promedios, el PDF usa (Total Prom. Ventas / Total Prom. Llevados) * 100
// Usaremos los promedios generales calculados aquí
porcentajeDevolucionGeneral: (totalDiasProm > 0 && (totalPonderadoLlevados / totalDiasProm) > 0)
? ((totalPonderadoVentas / totalDiasProm) / (totalPonderadoLlevados / totalDiasProm)) * 100
: 0,
});
setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal });
if (detalleCalculadoLocal.length === 0 && promediosCalculadoLocal.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);
// ... (manejo de errores)
} finally {
setLoading(false);
}
@@ -60,46 +171,61 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setError(null);
setApiErrorParams(null);
setCurrentParams(null);
setDetalleDiarioCalculado([]);
setPromediosPorDiaCalculado([]);
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.detalleSimple?.length && !reportData.promediosPorDia?.length)) {
if (!detalleDiarioCalculado.length && !promediosPorDiaCalculado.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,
if (detalleDiarioCalculado.length) {
const simpleToExport = detalleDiarioCalculado.map(({ id, ...rest }) => ({
"Día": rest.dia,
"Llevados": rest.llevados,
"Devueltos": rest.devueltos,
"Venta Neta": rest.ventaNeta,
"Promedio": rest.promedio,
"% Devolución": rest.porcentajeDevolucion,
}));
simpleToExport.push({
"Día": 0, "Llevados": totalesDetalle.llevados, "Devueltos": totalesDetalle.devueltos,
"Venta Neta": totalesDetalle.ventaNeta, "Promedio": totalesDetalle.promedioGeneralVentaNeta,
"% Devolución": totalesDetalle.porcentajeDevolucionGeneral,
});
const wsSimple = XLSX.utils.json_to_sheet(simpleToExport);
XLSX.utils.book_append_sheet(wb, wsSimple, "DetalleDiario");
XLSX.utils.book_append_sheet(wb, wsSimple, "DetalleDiarioCanillitas");
}
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,
if (promediosPorDiaCalculado.length) {
const promediosToExport = promediosPorDiaCalculado.map(({ id, ...rest }) => ({
"Día Semana": rest.dia,
"Cant. Días": rest.cant,
"Prom. Llevados": rest.promedio_Llevados,
"Prom. Devueltos": rest.promedio_Devueltos,
"Prom. Ventas": rest.promedio_Ventas,
"% Devolución": rest.porcentajeDevolucion,
}));
promediosToExport.push({
"Día Semana": "General", "Cant. Días": totalesPromedios.cantDias,
"Prom. Llevados": totalesPromedios.promLlevados, "Prom. Devueltos": totalesPromedios.promDevueltos,
"Prom. Ventas": totalesPromedios.promVentas, "% Devolución": totalesPromedios.porcentajeDevolucionGeneral
});
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDiaSemana");
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosCanillitas");
}
let fileName = "ListadoDistribucionCanillas";
let fileName = "ListadoDistribucionCanillitas";
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]);
}, [detalleDiarioCalculado, promediosPorDiaCalculado, currentParams, totalesDetalle, totalesPromedios]);
const handleGenerarYAbrirPdf = useCallback(async () => {
if (!currentParams) {
@@ -110,7 +236,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setError(null);
try {
const blob = await reportesService.getListadoDistribucionCanillasPdf(currentParams);
if (blob.type === "application/json") {
if (blob.type === "application/json") {
const text = await blob.text();
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
setError(msg);
@@ -126,6 +252,62 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
}
}, [currentParams]);
const columnsDetalle: GridColDef[] = [
{ field: 'dia', headerName: 'Día', type: 'number', width: 70, align: 'right', headerAlign: 'right', sortable: false },
{ field: 'llevados', headerName: 'Llevados', type: 'number', flex: 0.7, minWidth: 90, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', flex: 0.7, minWidth: 90, align: 'right', headerAlign: 'right', valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '0' },
{ field: 'ventaNeta', headerName: 'Venta Neta', type: 'number', flex: 0.8, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio', headerName: 'Promedio', type: 'number', flex: 0.8, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
{ field: 'porcentajeDevolucion', headerName: '% Devolución', type: 'number', flex: 0.8, minWidth: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => `${Number(value).toLocaleString('es-AR', { maximumFractionDigits: 2 })}%` },
];
const columnsPromedios: GridColDef[] = [
{ field: 'dia', headerName: 'Día Semana', width: 140, flex: 1 },
{ field: 'cant', headerName: 'Cant. Días', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio_Llevados', headerName: 'Prom. Llevados', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio_Devueltos', headerName: 'Prom. Devueltos', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio_Ventas', headerName: 'Prom. Ventas', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'porcentajeDevolucion', headerName: '% Devolución', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => `${Number(value).toLocaleString('es-AR', { maximumFractionDigits: 2 })}%` },
];
// --- Custom Footer para Detalle Diario ---
const CustomFooterDetalle = () => (
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
{/* Contenedor para los elementos del footer por defecto (paginación, etc.) */}
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}> {/* Permitir que se encoja un poco si es necesario */}
<GridFooter sx={{ borderTop: 'none' }} />
</Box>
{/* Contenedor para tus totales */}
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { maximumFractionDigits: 2 })}%</Typography>
</Box>
</GridFooterContainer>
);
// --- Custom Footer para Promedios por Día ---
const CustomFooterPromedios = () => (
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
<GridFooter sx={{ borderTop: 'none' }} />
</Box>
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
</Box>
</GridFooterContainer>
);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -157,69 +339,54 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
</Button>
</Box>
</Box>
<Typography variant="subtitle1" gutterBottom>
Publicación: {currentParams?.nombrePublicacion} |
Fechas: {currentParams?.fechaDesde} al {currentParams?.fechaHasta}
</Typography>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><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>)}
{detalleDiarioCalculado.length > 0 ? (
<Paper sx={{ height: 450, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={detalleDiarioCalculado} // Usar los datos calculados
columns={columnsDetalle}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterDetalle }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic' }}>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>)}
{promediosPorDiaCalculado.length > 0 ? (
<Paper sx={{ height: 360, width: '100%', '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={promediosPorDiaCalculado} // Usar los datos calculados
columns={columnsPromedios}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterPromedios }}
hideFooterSelectedRowCount
sx={{
'& .MuiTablePagination-root': { // Oculta el paginador por defecto
display: 'none',
},
}}
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de promedios por día.</Typography>)}
</>
)}
{!loading && !error && (!reportData || ((!reportData.detalleSimple || reportData.detalleSimple.length === 0) && (!reportData.promediosPorDia || reportData.promediosPorDia.length === 0))) &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};
export default ReporteListadoDistribucionCanillasPage;
export default ReporteListadoDistribucionCanillasPage; // Asegúrate de que el nombre del archivo coincida con este export

View File

@@ -1,50 +1,116 @@
import React, { useState, useCallback } from 'react';
// src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral';
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
interface TotalesComunes {
totalCantSalida: number;
totalCantEntrada: number;
vendidos: number;
totalRendir: number;
}
const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | 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)
fecha: string;
idEmpresa: number;
nombreEmpresa?: string;
} | null>(null);
const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
// Estados para los totales de cada sección
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistas, setTotalesAccionistas] = useState<TotalesComunes>(initialTotals);
const [totalesTodos, setTotalesTodos] = useState<TotalesComunes>(initialTotals);
const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState<TotalesComunes>(initialTotals);
// --- Formateadores ---
const currencyFormatter = (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
const numberFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const calculateAndSetTotals = (dataArray: Array<any> | undefined, setTotalsFunc: React.Dispatch<React.SetStateAction<TotalesComunes>>) => {
if (dataArray && dataArray.length > 0) {
const totals = dataArray.reduce((acc, item) => {
acc.totalCantSalida += Number(item.totalCantSalida) || 0;
acc.totalCantEntrada += Number(item.totalCantEntrada) || 0;
acc.totalRendir += Number(item.totalRendir) || 0;
return acc;
}, { totalCantSalida: 0, totalCantEntrada: 0, totalRendir: 0 });
totals.vendidos = totals.totalCantSalida - totals.totalCantEntrada;
setTotalsFunc(totals);
} else {
setTotalsFunc(initialTotals);
}
};
const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
fecha: string;
idEmpresa: number;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
setReportData(null); // Limpiar datos antiguos
// Resetear totales
setTotalesCanillas(initialTotals);
setTotalesAccionistas(initialTotals);
setTotalesTodos(initialTotals);
setTotalesCanillasOtraFecha(initialTotals);
setTotalesAccionistasOtraFecha(initialTotals);
// 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)) {
const data = await reportesService.getReporteDistribucionCanillas(params);
const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
(arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
const processedData = {
canillas: addIds(data.canillas, 'can'),
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
canillasTodos: addIds(data.canillasTodos, 'all'),
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'),
controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo')
};
setReportData(processedData);
// Calcular y setear totales para cada sección
calculateAndSetTotals(processedData.canillas, setTotalesCanillas);
calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas);
calculateAndSetTotals(processedData.canillasTodos, setTotalesTodos);
calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha);
calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha);
const noData = (!data.canillas || data.canillas.length === 0) &&
(!data.canillasAccionistas || data.canillasAccionistas.length === 0) &&
(!data.canillasTodos || data.canillasTodos.length === 0); // Podrías añadir más chequeos si es necesario
if (noData) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -68,62 +134,96 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) {
if (!reportData) {
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");
}
const formatAndSheet = (
data: any[],
sheetName: string,
fields: Record<string, string>,
totals?: TotalesComunes
) => {
if (data && data.length > 0) {
let exportedData = data.map(item => {
const row: Record<string, any> = {};
const { id, ...itemData } = item; // Excluir el 'id' generado
Object.keys(fields).forEach(key => {
row[fields[key]] = (itemData as any)[key];
if (key === 'fecha' && (itemData as any)[key]) {
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
}
if ((key === 'totalRendir') && (itemData as any)[key] != null) {
row[fields[key]] = parseFloat((itemData as any)[key]); // Mantener como número para suma en Excel
}
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
}
});
return row;
});
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");
}
if (totals) {
const totalRow: Record<string, any> = {};
const fieldKeys = Object.keys(fields);
totalRow[fields[fieldKeys[0]]] = "TOTALES"; // Título en la primera columna
if (fields.totalCantSalida) totalRow[fields.totalCantSalida] = totals.totalCantSalida;
if (fields.totalCantEntrada) totalRow[fields.totalCantEntrada] = totals.totalCantEntrada;
if (fields.vendidos) totalRow[fields.vendidos] = totals.vendidos;
if (fields.totalRendir) totalRow[fields.totalRendir] = totals.totalRendir;
exportedData.push(totalRow);
}
let fileName = "ListadoDistribucionGeneral";
const ws = XLSX.utils.json_to_sheet(exportedData);
const headers = Object.values(fields);
ws['!cols'] = headers.map(h => {
const maxLen = Math.max(...exportedData.map(row => (row[h]?.toString() ?? '').length), h.length);
return { wch: maxLen + 2 };
});
ws['!freeze'] = { xSplit: 0, ySplit: 1 }; // Congelar primera fila
XLSX.utils.book_append_sheet(wb, ws, sheetName);
}
};
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCtrlDevDetalle = { ingresados: "Ingresados", sobrantes: "Sobrantes", sinCargo: "Sin Cargo", publicacion: "Publicación", llevados: "Llevados", devueltos: "Devueltos", tipo: "Tipo" };
const fieldsCtrlDevRemitos = { remito: "Remito Ingresado" };
const fieldsCtrlDevOtrosDias = { devueltos: "Devueltos Otros Días" };
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista, totalesCanillas);
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista, totalesAccionistas);
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos, totalesTodos);
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq, totalesCanillasOtraFecha);
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq, totalesAccionistasOtraFecha);
formatAndSheet(reportData.controlDevolucionesDetalle, "CtrlDev_Detalle", fieldsCtrlDevDetalle); // Sin totales para estos
formatAndSheet(reportData.controlDevolucionesRemitos, "CtrlDev_Remitos", fieldsCtrlDevRemitos);
formatAndSheet(reportData.controlDevolucionesOtrosDias, "CtrlDev_OtrosDias", fieldsCtrlDevOtrosDias);
let fileName = "ReporteDetalleDistribucionCanillitas";
if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
fileName += `_${currentParams.fecha}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
}, [reportData, currentParams]);
}, [reportData, currentParams, totalesCanillas, totalesAccionistas, totalesTodos, totalesCanillasOtraFecha, totalesAccionistasOtraFecha]);
const handleGenerarYAbrirPdf = useCallback(async () => {
const handleGenerarYAbrirPdf = useCallback(async (soloTotales: boolean) => {
if (!currentParams) {
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
return;
}
setLoadingPdf(true);
setError(null);
setPdfSoloTotales(soloTotales);
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
const blob = await reportesService.getReporteDistribucionCanillasPdf({
...currentParams,
soloTotales
});
if (blob.type === "application/json") {
const text = await blob.text();
@@ -141,13 +241,99 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
}
}, [currentParams]);
// Definiciones de columnas
const commonColumns: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const commonColumnsWithFecha: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 },
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const columnsTodos: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const columnsCtrlDevDetalle: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.5 },
{ field: 'tipo', headerName: 'Tipo', width: 100, flex: 0.8 },
{ field: 'ingresados', headerName: 'Ingresados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'sobrantes', headerName: 'Sobrantes', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'sinCargo', headerName: 'Sin Cargo', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'llevados', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
];
const columnsCtrlDevRemitos: GridColDef[] = [
{ field: 'remito', headerName: 'Remito Ingresado', flex: 1 },
];
const columnsCtrlDevOtrosDias: GridColDef[] = [
{ field: 'devueltos', headerName: 'Devueltos Otros Días', flex: 1 },
];
// Memoizar filas (los IDs ya se añaden en handleGenerarReporte)
const rowsCanillas = useMemo(() => reportData?.canillas ?? [], [reportData]);
const rowsAccionistas = useMemo(() => reportData?.canillasAccionistas ?? [], [reportData]);
const rowsTodos = useMemo(() => reportData?.canillasTodos ?? [], [reportData]);
const rowsCanillasOtraFecha = useMemo(() => reportData?.canillasLiquidadasOtraFecha ?? [], [reportData]);
const rowsAccionistasOtraFecha = useMemo(() => reportData?.canillasAccionistasLiquidadasOtraFecha ?? [], [reportData]);
const rowsCtrlDevDetalle = useMemo(() => reportData?.controlDevolucionesDetalle ?? [], [reportData]);
const rowsCtrlDevRemitos = useMemo(() => reportData?.controlDevolucionesRemitos ?? [], [reportData]);
const rowsCtrlDevOtrosDias = useMemo(() => reportData?.controlDevolucionesOtrosDias ?? [], [reportData]);
// --- Custom Footers ---
// eslint-disable-next-line react/display-name
const createCustomFooter = (totals: TotalesComunes, columns: GridColDef[]) => () => (
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, minWidth: '300px' }}>
<GridFooter sx={{ borderTop: 'none' }} />
</Box>
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Typography variant="subtitle2" sx={{ flex: columns[0].flex, width: columns[0].width, textAlign: 'right', fontWeight: 'bold' }}>TOTALES:</Typography>
{columns[1].field !== 'tipoVendedor' && <Typography variant="subtitle2" sx={{ flex: columns[1].flex, width: columns[1].width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Canilla/Tipo */ }
{columns[1].field === 'tipoVendedor' && <Typography variant="subtitle2" sx={{ flex: columns[1].flex, width: columns[1].width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Canilla/Tipo */ }
{columns.find(c => c.field === 'fecha') && <Typography variant="subtitle2" sx={{ flex: columns.find(c=>c.field === 'fecha')?.flex, width: columns.find(c=>c.field === 'fecha')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Fecha */}
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalCantSalida')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.totalCantSalida)}</Typography>
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalCantEntrada')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.totalCantEntrada)}</Typography>
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'vendidos')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.vendidos)}</Typography>
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalRendir')?.width, textAlign: 'right', fontWeight: 'bold' }}>{currencyFormatter(totals.totalRendir)}</Typography>
</Box>
</GridFooterContainer>
);
const CustomFooterCanillas = useMemo(() => createCustomFooter(totalesCanillas, commonColumns), [totalesCanillas]);
const CustomFooterAccionistas = useMemo(() => createCustomFooter(totalesAccionistas, commonColumns), [totalesAccionistas]);
const CustomFooterTodos = useMemo(() => createCustomFooter(totalesTodos, columnsTodos), [totalesTodos]);
const CustomFooterCanillasOtraFecha = useMemo(() => createCustomFooter(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
const CustomFooterAccionistasOtraFecha = useMemo(() => createCustomFooter(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteListadoDistribucionGeneral
<SeleccionaReporteDetalleDistribucionCanillas
onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros}
onCancel={handleVolverAParametros} // Asumo que no se usa, ya que el selector no tiene botón de cancelar
isLoading={loading}
apiErrorMessage={apiErrorParams}
/>
@@ -159,10 +345,13 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
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"}
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha}</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
</Button>
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
Exportar a Excel
@@ -173,80 +362,151 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><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>)}
{/* Canillitas (del día) */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (del día)</Typography>
{rowsCanillas.length > 0 ? (
<Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsCanillas}
columns={commonColumns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterCanillas }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para canillitas (del día).</Typography>)}
{/* Accionistas (del día) */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (del día)</Typography>
{rowsAccionistas.length > 0 ? (
<Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsAccionistas}
columns={commonColumns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterAccionistas }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para accionistas (del día).</Typography>)}
{/* Resumen por Tipo de Vendedor (del día) */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen por Tipo de Vendedor (del día)</Typography>
{rowsTodos.length > 0 ? (
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsTodos}
columns={columnsTodos}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterTodos }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para resumen por tipo de vendedor (del día).</Typography>)}
{/* Canillitas (Liquidados de Otras Fechas) */}
{rowsCanillasOtraFecha.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (Liquidados de Otras Fechas)</Typography>
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsCanillasOtraFecha}
columns={commonColumnsWithFecha}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterCanillasOtraFecha }}
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
{/* Accionistas (Liquidados de Otras Fechas) */}
{rowsAccionistasOtraFecha.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (Liquidados de Otras Fechas)</Typography>
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsAccionistasOtraFecha}
columns={commonColumnsWithFecha}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterAccionistasOtraFecha }}
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
{/* Control Devoluciones - Detalle */}
{rowsCtrlDevDetalle.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Detalle</Typography>
<Paper sx={{ height: 300, width: '100%', mb: 3 }}>
<DataGrid
rows={rowsCtrlDevDetalle}
columns={columnsCtrlDevDetalle}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
hideFooterSelectedRowCount // Sin footer personalizado para estos
/>
</Paper>
</>
)}
{/* Control Devoluciones - Remitos */}
{rowsCtrlDevRemitos.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Remitos Ingresados</Typography>
<Paper sx={{ height: 200, width: '100%', mb: 3 }}>
<DataGrid
rows={rowsCtrlDevRemitos}
columns={columnsCtrlDevRemitos}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
autoHeight
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
{/* Control Devoluciones - Otros Días */}
{rowsCtrlDevOtrosDias.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Otros Días</Typography>
<Paper sx={{ height: 200, width: '100%', mb: 3 }}>
<DataGrid
rows={rowsCtrlDevOtrosDias}
columns={columnsCtrlDevOtrosDias}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
autoHeight
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
<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>)}
</>
)}
{!loading && !error && (!reportData ||
(rowsCanillas.length === 0 && rowsAccionistas.length === 0 && rowsTodos.length === 0 &&
rowsCanillasOtraFecha.length === 0 && rowsAccionistasOtraFecha.length === 0 &&
rowsCtrlDevDetalle.length === 0 && rowsCtrlDevRemitos.length === 0 && rowsCtrlDevOtrosDias.length === 0
)) &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)
}
</Box>
);
};
export default ReporteListadoDistribucionGeneralPage;
export default ReporteDetalleDistribucionCanillasPage;

View File

@@ -0,0 +1,421 @@
// src/pages/Reportes/ReporteListadoDistribucionPage.tsx
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid'; // No necesitas type Theme aquí
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionDistribuidoresResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionDistribuidoresResponseDto';
import SeleccionaReporteListadoDistribucion from './SeleccionaReporteListadoDistribucion';
import * as XLSX from 'xlsx';
import axios from 'axios';
const ReporteListadoDistribucionPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionDistribuidoresResponseDto | 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<{
idDistribuidor: number;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
nombrePublicacion?: string;
nombreDistribuidor?: string;
} | null>(null);
// --- ESTADO PARA TOTALES CALCULADOS (PARA EL FOOTER DEL DETALLE) ---
const [totalesDetalle, setTotalesDetalle] = useState({
llevados: 0,
devueltos: 0,
ventaNeta: 0,
promedioGeneralVentaNeta: 0,
porcentajeDevolucionGeneral: 0,
});
const [totalesPromedios, setTotalesPromedios] = useState({
cantDias: 0,
promLlevados: 0,
promDevueltos: 0,
promVentas: 0,
porcentajeDevolucionGeneral: 0,
});
const handleGenerarReporte = useCallback(async (params: {
idDistribuidor: number;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
setReportData(null);
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 });
setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0});
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
const distService = (await import('../../services/Distribucion/distribuidorService')).default;
const [pubData, distData] = await Promise.all([
pubService.getPublicacionById(params.idPublicacion),
distService.getDistribuidorById(params.idDistribuidor)
]);
setCurrentParams({
...params,
nombrePublicacion: pubData.nombre,
nombreDistribuidor: distData.nombre
});
try {
const data = await reportesService.getListadoDistribucionDistribuidores(params);
let acumuladoVentaNeta = 0;
let diasConVenta = 0;
const detalleConCalculos = data.detalleSimple.map((item, index) => {
const llevados = item.llevados || 0;
const devueltos = item.devueltos || 0;
const ventaNeta = llevados - devueltos;
if (llevados > 0) diasConVenta++; // O si ventaNeta > 0, dependiendo de la definición de "día con actividad"
acumuladoVentaNeta += ventaNeta;
return {
...item,
id: `simple-${index}`,
ventaNeta: ventaNeta,
promedio: diasConVenta > 0 ? acumuladoVentaNeta / diasConVenta : 0, // Promedio acumulativo hasta esa fila
porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0,
};
});
const totalLlevadosDetalle = detalleConCalculos.reduce((sum, item) => sum + (item.llevados || 0), 0);
const totalDevueltosDetalle = detalleConCalculos.reduce((sum, item) => sum + (item.devueltos || 0), 0);
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
setTotalesDetalle({
llevados: totalLlevadosDetalle,
devueltos: totalDevueltosDetalle,
ventaNeta: totalVentaNetaDetalle,
promedioGeneralVentaNeta: diasConVenta > 0 ? totalVentaNetaDetalle / diasConVenta : 0,
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
});
const promediosConCalculos = data.promediosPorDia.map((item, index) => ({
...item,
id: `prom-${index}`,
porcentajeDevolucion: item.promedio_Llevados > 0 ? (item.promedio_Devueltos / item.promedio_Llevados) * 100 : 0,
}));
// Calcular totales para la tabla de promedios (ponderados por Cant. Días)
const totalDiasPromedios = promediosConCalculos.reduce((sum, item) => sum + (item.cant || 0), 0);
const totalPonderadoLlevados = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
const totalPonderadoDevueltos = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0);
const totalPonderadoVentas = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
setTotalesPromedios({
cantDias: totalDiasPromedios,
promLlevados: totalDiasPromedios > 0 ? totalPonderadoLlevados / totalDiasPromedios : 0,
promDevueltos: totalDiasPromedios > 0 ? totalPonderadoDevueltos / totalDiasPromedios : 0,
promVentas: totalDiasPromedios > 0 ? totalPonderadoVentas / totalDiasPromedios : 0,
porcentajeDevolucionGeneral: totalPonderadoLlevados > 0 ? (totalPonderadoDevueltos / totalPonderadoLlevados) * 100 : 0,
});
setReportData({ detalleSimple: detalleConCalculos, promediosPorDia: promediosConCalculos });
if (detalleConCalculos.length === 0 && promediosConCalculos.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);
} 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(({ id, ...rest }) => ({ // Excluir saldoAcumulado del excel de detalle
"Día": rest.dia,
"Llevados": rest.llevados,
"Devueltos": rest.devueltos,
"Venta Neta": (rest.llevados || 0) - (rest.devueltos || 0),
"Promedio": (rest as any).promedio, // Ya calculado
"% Devolución": (rest as any).porcentajeDevolucion, // Ya calculado
}));
// Fila de totales para detalle
simpleToExport.push({
"Día": 0, // Usar 0 para cumplir con el tipo number requerido
"Llevados": totalesDetalle.llevados,
"Devueltos": totalesDetalle.devueltos,
"Venta Neta": totalesDetalle.ventaNeta,
"Promedio": totalesDetalle.promedioGeneralVentaNeta, // Usar el promedio general calculado
"% Devolución": totalesDetalle.porcentajeDevolucionGeneral,
});
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(({ id, ...rest }) => ({
"Día Semana": rest.dia,
"Cant. Días": rest.cant,
"Prom. Llevados": rest.promedio_Llevados,
"Prom. Devueltos": rest.promedio_Devueltos,
"Prom. Ventas": rest.promedio_Ventas,
"% Devolución": (rest as any).porcentajeDevolucion, // Ya calculado
}));
// Fila de totales para promedios
promediosToExport.push({
"Día Semana": "General",
"Cant. Días": totalesPromedios.cantDias,
"Prom. Llevados": totalesPromedios.promLlevados,
"Prom. Devueltos": totalesPromedios.promDevueltos,
"Prom. Ventas": totalesPromedios.promVentas,
"% Devolución": totalesPromedios.porcentajeDevolucionGeneral,
});
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDiaSemana");
}
let fileName = "ListadoDistribucionDistribuidores";
if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
fileName += `_Dist${currentParams.nombreDistribuidor?.replace(/\s+/g, '') ?? currentParams.idDistribuidor}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
}, [reportData, currentParams, totalesDetalle, totalesPromedios]);
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.getListadoDistribucionDistribuidoresPdf(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]);
const columnsDetalle: GridColDef[] = [
{ field: 'dia', headerName: 'Día', type: 'number', width: 70, align: 'right', headerAlign: 'right', sortable: false }, // Usar width fijo para columnas pequeñas
{ field: 'llevados', headerName: 'Llevados', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '0' },
{ field: 'ventaNeta', headerName: 'Venta Neta', type: 'number', flex: 1, minWidth: 110, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.llevados || 0) - (row.devueltos || 0), valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio', headerName: 'Promedio', type: 'number', flex: 1, minWidth: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { minimumFractionDigits: 3, maximumFractionDigits: 3 }) },
{ field: 'porcentajeDevolucion', headerName: '% Devolución', type: 'number', flex: 1, minWidth: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => `${Number(value).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%` },
];
const columnsPromedios: GridColDef[] = [
{ field: 'dia', headerName: 'Día Semana', width: 140, flex: 1.2 }, // Un poco más de espacio para el nombre del día
{ field: 'cant', headerName: 'Cant. Días', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio_Llevados', headerName: 'Prom. Llevados', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio_Devueltos', headerName: 'Prom. Devueltos', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'promedio_Ventas', headerName: 'Prom. Ventas', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
{ field: 'porcentajeDevolucion', headerName: '% Devolución', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => `${Number(value).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%` },
];
const rowsDetalle = useMemo(() => reportData?.detalleSimple ?? [], [reportData]);
const rowsPromedios = useMemo(() => reportData?.promediosPorDia ?? [], [reportData]);
// --- Custom Footer para Detalle Diario ---
const CustomFooterDetalle = () => (
<GridFooterContainer
sx={{
// Asegurar que el contenedor pueda usar todo el ancho
// y que los items internos puedan distribuirse.
// justifyContent: 'space-between' // Esto podría ayudar
}}
>
{/* Contenedor para los elementos del footer por defecto (paginación, etc.) */}
<Box sx={{
// flexGrow: 1, // Originalmente teníamos esto, puede ser muy agresivo
display: 'flex', // Para alinear los items del paginador por defecto
alignItems: 'center',
// Intenta darle un ancho mínimo o un flex-basis para que tenga espacio antes que los totales
minWidth: '400px', // AJUSTA ESTE VALOR según lo que necesites
flexShrink: 0, // Evita que se encoja demasiado si los totales son muy anchos
}}>
<GridFooter sx={{
borderTop: 'none',
// '& .MuiTablePagination-toolbar': { // Para los elementos dentro del paginador
// flexWrap: 'wrap', // Permitir que los elementos internos del paginador se envuelvan
// justifyContent: 'flex-start',
// },
// '& .MuiTablePagination-spacer': { // El espaciador puede ser un problema
// display: 'none', // Prueba quitándolo
// }
}} />
</Box>
{/* Contenedor para tus totales, alineado a la derecha */}
<Box sx={{
p: 1,
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
marginLeft: 'auto', // Empuja a la derecha
flexShrink: 1, // Permite que este se encoja si es necesario, pero no demasiado
overflowX: 'auto', // Si los totales son muchos, que tengan su propio scroll
whiteSpace: 'nowrap', // Evitar que los textos de totales se partan
}}>
{/* Mantén esta estructura, pero quizás necesitas jugar con los minWidth/flex de los Typography */}
<Typography variant="subtitle2" sx={{ flexBasis: columnsPromedios[0].width || 'auto', minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>Generales:</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { minimumFractionDigits: 3, maximumFractionDigits: 3 })}</Typography>
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
</Box>
</GridFooterContainer>
);
// --- Custom Footer para Promedios por Día (Ajustado para flex) ---
const CustomFooterPromedios = () => (
<GridFooterContainer sx={{ /* justifyContent: 'space-between' */ }}>
<Box sx={{
// flexGrow: 1,
display: 'flex',
alignItems: 'center',
minWidth: '400px', // AJUSTA ESTE VALOR
flexShrink: 0,
}}>
<GridFooter sx={{ borderTop: 'none' }} />
</Box>
<Box sx={{
p: 1,
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
marginLeft: 'auto',
flexShrink: 1,
overflowX: 'auto',
whiteSpace: 'nowrap',
}}>
<Typography variant="subtitle2" sx={{ flexBasis: columnsPromedios[0].width || 'auto', minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>Generales:</Typography>
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[2].flex, minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[3].flex, minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[4].flex, minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[5].flex, minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
</Box>
</GridFooterContainer>
);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteListadoDistribucion
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 ({currentParams?.nombreDistribuidor})</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>
<Typography variant="subtitle1" gutterBottom>
Publicación: {currentParams?.nombrePublicacion} |
Fechas: {currentParams?.fechaDesde} al {currentParams?.fechaHasta}
</Typography>
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><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>
{rowsDetalle.length > 0 ? (
<Paper sx={{ height: 400, width: '100%', mb: 3 }}> {/* Ajusta altura según necesites */}
<DataGrid
rows={rowsDetalle}
columns={columnsDetalle}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterDetalle }}
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de detalle diario.</Typography>)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
{rowsPromedios.length > 0 ? (
<Paper sx={{ height: 400, width: '100%' }}> {/* Ajusta altura según necesites */}
<DataGrid
rows={rowsPromedios}
columns={columnsPromedios}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterPromedios }}
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de promedios por día.</Typography>)}
</>
)}
{!loading && !error && (!reportData || ((!reportData.detalleSimple || reportData.detalleSimple.length === 0) && (!reportData.promediosPorDia || reportData.promediosPorDia.length === 0))) &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};
export default ReporteListadoDistribucionPage;

View File

@@ -1,14 +1,27 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
// Corregir importaciones de DTOs
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
import type { MovimientoBobinaEstadoDetalleDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto';
import type { MovimientoBobinaEstadoTotalDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoTotalDto';
import SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Interfaces extendidas para DataGrid con 'id'
interface DetalleMovimientoDataGrid extends MovimientoBobinaEstadoDetalleDto { // Usar el DTO correcto
id: string;
}
interface TotalPorEstadoDataGrid extends MovimientoBobinaEstadoTotalDto { // Usar el DTO correcto
id: string;
}
const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null);
const [loading, setLoading] = useState(false);
@@ -20,8 +33,16 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
nombrePlanta?: string;
} | null>(null);
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const dateLocaleFormatter = (value: string | null | undefined) =>
value ? new Date(value).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-';
const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string;
fechaHasta: string;
@@ -33,8 +54,14 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
setCurrentParams(params);
try {
const data = await reportesService.getMovimientoBobinasEstado(params);
setReportData(data);
if ((!data.detalle || data.detalle.length === 0) && (!data.totales || data.totales.length === 0)) {
const processedData: MovimientoBobinasPorEstadoResponseDto = {
detalle: data.detalle?.map((item, index) => ({ ...item, id: `detalle-${index}-${item.numeroRemito}-${item.tipoBobina}` })) || [],
totales: data.totales?.map((item, index) => ({ ...item, id: `total-${index}-${item.tipoMovimiento}` })) || []
};
setReportData(processedData);
if ((!processedData.detalle || processedData.detalle.length === 0) && (!processedData.totales || processedData.totales.length === 0)) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -65,7 +92,6 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
const wb = XLSX.utils.book_new();
// Hoja de Detalles
if (reportData.detalle?.length) {
const detalleToExport = reportData.detalle.map(item => ({
"Tipo Bobina": item.tipoBobina,
@@ -76,18 +102,11 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
}));
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['!cols'] = headersDetalle.map(h => ({ wch: Math.max(...detalleToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 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,
@@ -96,20 +115,15 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
}));
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['!cols'] = headersTotales.map(h => ({ wch: Math.max(...totalesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 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 += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
@@ -140,6 +154,56 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
}
}, [currentParams]);
// Columnas para DataGrid de Detalle de Movimientos
const columnsDetalle: GridColDef<DetalleMovimientoDataGrid>[] = [ // Tipar con la interfaz correcta
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 220, flex: 1.5 },
{ field: 'numeroRemito', headerName: 'Nro Remito', width: 130, flex: 0.8 },
{ field: 'fechaMovimiento', headerName: 'Fecha Movimiento', width: 150, flex: 1, valueFormatter: (value) => dateLocaleFormatter(value as string) },
{ field: 'cantidad', headerName: 'Cantidad', type: 'number', width: 120, align: 'right', headerAlign: 'right', flex: 0.7, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'tipoMovimiento', headerName: 'Tipo Movimiento', width: 150, flex: 1 },
];
// Columnas para DataGrid de Totales por Estado
const columnsTotales: GridColDef<TotalPorEstadoDataGrid>[] = [ // Tipar con la interfaz correcta
{ field: 'tipoMovimiento', headerName: 'Tipo Movimiento', width: 200, flex: 1 },
{ field: 'totalBobinas', headerName: 'Total Bobinas', type: 'number', width: 150, align: 'right', headerAlign: 'right', flex: 0.8, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'totalKilos', headerName: 'Total Kilos', type: 'number', width: 150, align: 'right', headerAlign: 'right', flex: 0.8, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
];
const rowsDetalle = useMemo(() => (reportData?.detalle as DetalleMovimientoDataGrid[]) || [], [reportData]);
const rowsTotales = useMemo(() => (reportData?.totales as TotalPorEstadoDataGrid[]) || [], [reportData]);
// eslint-disable-next-line react/display-name
const CustomFooterDetalle = () => (
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
}}>
<Box sx={{
flexGrow:1,
display:'flex',
justifyContent:'flex-start',
}}>
<GridFooter
sx={{
borderTop: 'none',
width: 'auto',
'& .MuiToolbar-root': {
paddingLeft: (theme) => theme.spacing(1),
paddingRight: (theme) => theme.spacing(1),
},
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
</GridFooterContainer>
);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -154,11 +218,12 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
</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>
<Typography variant="h5">Reporte: Movimiento de Bobinas por Estado {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
onClick={handleGenerarYAbrirPdf}
@@ -182,74 +247,51 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" 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>
{rowsDetalle.length > 0 ? (
<Paper sx={{ width: '100%', mb: 3 }}>
<DataGrid
rows={rowsDetalle}
columns={columnsDetalle}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
sx={{ height: 'calc(100vh - 350px)' }}
slots={{ footer: CustomFooterDetalle }}
/>
</Paper>
) : (
<Typography sx={{ mb: 3 }}>No hay detalles de movimientos para mostrar.</Typography>
<Typography sx={{ mb: 3, fontStyle: 'italic' }}>No hay detalles de movimientos para mostrar.</Typography>
)}
{/* Tabla de Totales por Estado */}
<Typography variant="h6" gutterBottom>
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
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>
{rowsTotales.length > 0 ? (
<Paper sx={{ width: '100%', maxWidth: '700px' }}>
<DataGrid
rows={rowsTotales}
columns={columnsTotales}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
autoHeight
hideFooter
disableRowSelectionOnClick
/>
</Paper>
) : (
<Typography>No hay totales para mostrar.</Typography>
<Typography sx={{fontStyle: 'italic'}}>No hay totales por estado para mostrar.</Typography>
)}
</>
)}
{!loading && !error && !reportData && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};

View File

@@ -1,16 +1,22 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid'; // Importaciones para DataGrid
import { esES } from '@mui/x-data-grid/locales'; // Para localización
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';
// Definición de la interfaz extendida para DataGrid (con 'id')
interface MovimientoBobinasDataGridDto extends MovimientoBobinasDto {
id: string;
}
const ReporteMovimientoBobinasPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasDto[]>([]);
const [reportData, setReportData] = useState<MovimientoBobinasDataGridDto[]>([]); // Usar el tipo extendido
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -20,8 +26,12 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
nombrePlanta?: string;
} | null>(null);
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string;
fechaHasta: string;
@@ -30,11 +40,20 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
setLoading(true);
setError(null);
setApiErrorParams(null);
// Opcional: Obtener nombre de la planta
// const plantaService = (await import('../../services/Maestros/plantaService')).default;
// const plantaData = await plantaService.getPlantaById(params.idPlanta);
// setCurrentParams({...params, nombrePlanta: plantaData?.nombre});
setCurrentParams(params);
try {
const data = await reportesService.getMovimientoBobinas(params);
setReportData(data);
if (data.length === 0) {
// Añadir 'id' único a cada fila para DataGrid
const dataWithIds = data.map((item, index) => ({
...item,
id: `${item.tipoBobina}-${index}` // Asumiendo que tipoBobina es único por reporte o combinar con index
}));
setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -64,18 +83,35 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
}
const dataToExport = reportData.map(item => ({
"Tipo Bobina": item.tipoBobina,
"Bobinas Iniciales": item.bobinasIniciales,
"Cant. Inicial": item.bobinasIniciales,
"Kg Iniciales": item.kilosIniciales,
"Bobinas Compradas": item.bobinasCompradas,
"Compradas": item.bobinasCompradas,
"Kg Comprados": item.kilosComprados,
"Bobinas Consumidas": item.bobinasConsumidas,
"Consumidas": item.bobinasConsumidas,
"Kg Consumidos": item.kilosConsumidos,
"Bobinas Dañadas": item.bobinasDaniadas,
"Dañadas": item.bobinasDaniadas,
"Kg Dañados": item.kilosDaniados,
"Bobinas Finales": item.bobinasFinales,
"Cant. Final": item.bobinasFinales,
"Kg Finales": item.kilosFinales,
}));
// Añadir fila de totales
const totalesRow = {
"Tipo Bobina": "Totales",
"Cant. Inicial": reportData.reduce((sum, item) => sum + item.bobinasIniciales, 0),
"Kg Iniciales": reportData.reduce((sum, item) => sum + item.kilosIniciales, 0),
"Compradas": reportData.reduce((sum, item) => sum + item.bobinasCompradas, 0),
"Kg Comprados": reportData.reduce((sum, item) => sum + item.kilosComprados, 0),
"Consumidas": reportData.reduce((sum, item) => sum + item.bobinasConsumidas, 0),
"Kg Consumidos": reportData.reduce((sum, item) => sum + item.kilosConsumidos, 0),
"Dañadas": reportData.reduce((sum, item) => sum + item.bobinasDaniadas, 0),
"Kg Dañados": reportData.reduce((sum, item) => sum + item.kilosDaniados, 0),
"Cant. Final": reportData.reduce((sum, item) => sum + item.bobinasFinales, 0),
"Kg Finales": reportData.reduce((sum, item) => sum + item.kilosFinales, 0),
};
dataToExport.push(totalesRow);
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0]);
ws['!cols'] = headers.map(h => {
@@ -91,7 +127,9 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
XLSX.utils.book_append_sheet(wb, ws, "MovimientoBobinas");
let fileName = "ReporteMovimientoBobinas";
if (currentParams) {
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_Planta${currentParams.idPlanta}`;
// Asumiendo que currentParams.nombrePlanta está disponible o se usa idPlanta
fileName += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
@@ -122,6 +160,137 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
}
}, [currentParams]);
// Definiciones de Columnas para DataGrid
const columns: GridColDef[] = [
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 200, flex: 1.5 },
{ field: 'bobinasIniciales', headerName: 'Cant. Ini.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosIniciales', headerName: 'Kg Ini.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasCompradas', headerName: 'Compradas', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosComprados', headerName: 'Kg Compr.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasConsumidas', headerName: 'Consum.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosConsumidos', headerName: 'Kg Consum.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasDaniadas', headerName: 'Dañadas', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosDaniados', headerName: 'Kg Dañ.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'bobinasFinales', headerName: 'Cant. Fin.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'kilosFinales', headerName: 'Kg Finales', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
];
const rows = useMemo(() => reportData, [reportData]);
// Calcular totales para el footer
const totales = useMemo(() => {
if (reportData.length === 0) return null;
return {
bobinasIniciales: reportData.reduce((sum, item) => sum + item.bobinasIniciales, 0),
kilosIniciales: reportData.reduce((sum, item) => sum + item.kilosIniciales, 0),
bobinasCompradas: reportData.reduce((sum, item) => sum + item.bobinasCompradas, 0),
kilosComprados: reportData.reduce((sum, item) => sum + item.kilosComprados, 0),
bobinasConsumidas: reportData.reduce((sum, item) => sum + item.bobinasConsumidas, 0),
kilosConsumidos: reportData.reduce((sum, item) => sum + item.kilosConsumidos, 0),
bobinasDaniadas: reportData.reduce((sum, item) => sum + item.bobinasDaniadas, 0),
kilosDaniados: reportData.reduce((sum, item) => sum + item.kilosDaniados, 0),
bobinasFinales: reportData.reduce((sum, item) => sum + item.bobinasFinales, 0),
kilosFinales: reportData.reduce((sum, item) => sum + item.kilosFinales, 0),
};
}, [reportData]);
// eslint-disable-next-line react/display-name
const CustomFooter = () => {
if (!totales) return null;
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
const colConfig = columns.find(c => c.field === field);
let targetWidth: number | string = 'auto'; // Por defecto, dejar que el contenido decida
let targetMinWidth: number | string = 'auto';
if (isLabel) {
// Para la etiqueta "TOTALES:", un ancho más ajustado.
// Podrías basarlo en el ancho de la primera columna si es consistentemente la de "Tipo Bobina"
// o un valor fijo que sepas que funciona.
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.6) : 120; // Ej: 60% del ancho de la columna o 120px
targetMinWidth = 80; // Un mínimo razonable para "TOTALES:"
} else if (colConfig) {
// Para los valores numéricos, podemos ser un poco más conservadores que el ancho de la columna.
// O usar el ancho de la columna si es pequeño.
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.85) : 90; // Ej: 85% del ancho de la columna o 90px
targetMinWidth = 70; // Un mínimo para números
}
return {
minWidth: targetMinWidth,
width: targetWidth,
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'right' | 'left' | 'center',
pr: isLabel ? 1 : (field === 'kilosFinales' ? 0 : 1), // padding-right
fontWeight: 'bold',
// Añadimos overflow y textOverflow para manejar texto largo en la etiqueta si fuera necesario
overflow: isLabel ? 'hidden' : undefined,
textOverflow: isLabel ? 'ellipsis' : undefined,
whiteSpace: 'nowrap', // Asegurar que no haya saltos de línea en los totales
};
};
return (
<GridFooterContainer sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
minHeight: '52px',
}}>
{/* Box para la paginación estándar */}
<Box sx={{
display: 'flex',
alignItems: 'center',
flexShrink: 0,
overflow: 'hidden',
px:1,
// Para asegurar que la paginación no se coma todo el espacio si es muy ancha:
// Podríamos darle un flex-basis o un maxWidth si los totales necesitan más espacio garantizado.
// Por ejemplo:
// flexBasis: '50%', // Ocupa el 50% del espacio disponible si no hay otros factores
// maxWidth: '600px', // Un máximo absoluto
}}>
<GridFooter
sx={{
borderTop: 'none',
width: '100%',
'& .MuiToolbar-root': {
paddingLeft: 0,
paddingRight: 0,
},
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
{/* Box para los totales personalizados */}
<Box sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
whiteSpace: 'nowrap', // Ya estaba, es importante
overflowX: 'auto',
px:1,
flexShrink: 1, // Permitir que este contenedor se encoja si es necesario
// maxWidth: 'calc(100% - ANCHO_PAGINACION_ESTIMADO)' // Si quieres ser muy preciso
}}>
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasIniciales')}>{numberLocaleFormatter(totales.bobinasIniciales)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosIniciales')}>{numberLocaleFormatter(totales.kilosIniciales)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasCompradas')}>{numberLocaleFormatter(totales.bobinasCompradas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosComprados')}>{numberLocaleFormatter(totales.kilosComprados)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasConsumidas')}>{numberLocaleFormatter(totales.bobinasConsumidas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosConsumidos')}>{numberLocaleFormatter(totales.kilosConsumidos)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasDaniadas')}>{numberLocaleFormatter(totales.bobinasDaniadas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosDaniados')}>{numberLocaleFormatter(totales.kilosDaniados)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('bobinasFinales')}>{numberLocaleFormatter(totales.bobinasFinales)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('kilosFinales')}>{numberLocaleFormatter(totales.kilosFinales)}</Typography>
</Box>
</GridFooterContainer>
);
};
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -140,7 +309,7 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
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>
<Typography variant="h5">Reporte: Movimiento de Bobinas {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
onClick={handleGenerarYAbrirPdf}
@@ -164,47 +333,24 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" 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>
{!loading && !error && reportData.length > 0 && (
<Paper sx={{ width: '100%', mt: 2 }}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
slots={{ footer: CustomFooter }}
density="compact"
autoHeight // Para que se ajuste al contenido y al footer
hideFooterSelectedRowCount
disableRowSelectionOnClick
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};

View File

@@ -1,16 +1,22 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
Box, Typography, Paper, CircularProgress, Alert, Button
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { TiradasPublicacionesSeccionesDto } from '../../models/dtos/Reportes/TiradasPublicacionesSeccionesDto';
import SeleccionaReporteTiradasPublicacionesSecciones from './SeleccionaReporteTiradasPublicacionesSecciones';
import * as XLSX from 'xlsx';
import axios from 'axios';
// Interfaz extendida para DataGrid
interface TiradasPublicacionesSeccionesDataGridDto extends TiradasPublicacionesSeccionesDto {
id: string;
}
const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
const [reportData, setReportData] = useState<TiradasPublicacionesSeccionesDto[]>([]);
const [reportData, setReportData] = useState<TiradasPublicacionesSeccionesDataGridDto[]>([]);
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -27,13 +33,16 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
mesAnioParaNombreArchivo?: string;
} | null>(null);
const numberLocaleFormatter = (value: number | null | undefined, fractionDigits: number = 0) =>
value != null ? Number(value).toLocaleString('es-AR', { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits }) : '';
const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}) => {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
@@ -47,15 +56,22 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
const plantaData = await plantaService.getPlantaById(params.idPlanta);
plantaNombre = plantaData?.nombre ?? "N/A";
}
const mesAnioParts = params.fechaDesde.split('-');
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
// Formatear mes y año para el título/nombre de archivo
// const fechaDesdeObj = new Date(params.fechaDesde + 'T00:00:00'); // Asegurar que es local
// const mesAnioNombre = fechaDesdeObj.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
// Usar el formato mes/año del PDF para el nombre del archivo si se prefiere, o el que ya tenías
const dateParts = params.fechaDesde.split('-'); // Asume YYYY-MM-DD
const mesAnioNombre = `${dateParts[1]}/${dateParts[0]}`;
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, nombrePlanta: plantaNombre, mesAnioParaNombreArchivo: mesAnioNombre});
try {
const data = await reportesService.getTiradasPublicacionesSecciones(params);
setReportData(data);
if (data.length === 0) {
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.nombreSeccion}-${index}` }));
setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -84,13 +100,24 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
}
const dataToExport = reportData.map(item => ({
"Nombre Sección": item.nombreSeccion,
"Total Páginas Impresas": item.totalPaginasImpresas,
"Cantidad Ediciones": item.cantidadTiradas,
"Total Páginas x Edición": item.totalPaginasEjemplares,
"Páginas Impresas": item.totalPaginasImpresas, // Cambiado para coincidir con PDF
"Total Ediciones": item.cantidadTiradas, // Cambiado para coincidir con PDF
"Pág. Por Edición (Promedio)": item.totalPaginasEjemplares, // Nombre según PDF
"Total Ejemplares": item.totalEjemplares,
"Prom. Pág./Ejemplar": item.promedioPaginasPorEjemplar,
"Pág. Ejemplar (Promedio)": item.promedioPaginasPorEjemplar, // Nombre según PDF
}));
// Totales para Excel
const totales = {
"Nombre Sección": "Totales",
"Páginas Impresas": reportData.reduce((sum, item) => sum + item.totalPaginasImpresas, 0),
"Total Ediciones": reportData.reduce((sum, item) => sum + item.cantidadTiradas, 0),
"Pág. Por Edición (Promedio)": reportData.reduce((sum, item) => sum + item.totalPaginasEjemplares, 0), // Suma de promedios para el total, como en el PDF
"Total Ejemplares": reportData.reduce((sum, item) => sum + item.totalEjemplares, 0),
"Pág. Ejemplar (Promedio)": reportData.reduce((sum, item) => sum + item.promedioPaginasPorEjemplar, 0), // Suma de promedios para el total
};
dataToExport.push(totales);
const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => {
@@ -105,8 +132,8 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
let fileName = "ReporteTiradasSecciones";
if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
if (!currentParams.consolidado) fileName += `_Planta${currentParams.idPlanta}`;
else fileName += "_Consolidado";
if (!currentParams.consolidado && currentParams.idPlanta) fileName += `_Planta${currentParams.idPlanta}`;
else if (currentParams.consolidado) fileName += "_Consolidado";
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
}
fileName += ".xlsx";
@@ -138,6 +165,83 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
}
}, [currentParams]);
const columns: GridColDef<TiradasPublicacionesSeccionesDataGridDto>[] = [
{ field: 'nombreSeccion', headerName: 'Nombre', width: 250, flex: 1.5 },
{ field: 'totalPaginasImpresas', headerName: 'Páginas Impresas', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'cantidadTiradas', headerName: 'Total Ediciones', type: 'number', width: 130, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'totalPaginasEjemplares', headerName: 'Pág. Por Edición (Promedio)', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'totalEjemplares', headerName: 'Total Ejemplares', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
{ field: 'promedioPaginasPorEjemplar', headerName: 'Pág. Ejemplar (Promedio)', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
];
const rows = useMemo(() => reportData, [reportData]);
const totalesGenerales = useMemo(() => {
if (reportData.length === 0) return null;
return {
totalPaginasImpresas: reportData.reduce((sum, item) => sum + item.totalPaginasImpresas, 0),
cantidadTiradas: reportData.reduce((sum, item) => sum + item.cantidadTiradas, 0),
totalPaginasEjemplares: reportData.reduce((sum, item) => sum + item.totalPaginasEjemplares, 0), // Suma de promedios para el total, como en el PDF
totalEjemplares: reportData.reduce((sum, item) => sum + item.totalEjemplares, 0),
promedioPaginasPorEjemplar: reportData.reduce((sum, item) => sum + item.promedioPaginasPorEjemplar, 0), // Suma de promedios para el total
};
}, [reportData]);
// eslint-disable-next-line react/display-name
const CustomFooter = () => {
if (!totalesGenerales) return null;
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
const colConfig = columns.find(c => c.field === field);
let targetWidth: number | string = 'auto';
let targetMinWidth: number | string = 'auto';
if (isLabel) {
targetWidth = colConfig?.width ? Math.max(100, colConfig.width * 0.6) : 150;
targetMinWidth = 100;
} else if (colConfig) {
targetWidth = colConfig.width ? Math.max(80, colConfig.width * 0.8) : 100;
targetMinWidth = 80;
}
return {
minWidth: targetMinWidth,
width: targetWidth,
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
pr: isLabel ? 1 : (field === 'promedioPaginasPorEjemplar' ? 0 : 1),
fontWeight: 'bold',
whiteSpace: 'nowrap',
};
};
return (
<GridFooterContainer sx={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
width: '100%', borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
}}>
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, overflow: 'hidden', px:1 }}>
<GridFooter
sx={{ borderTop: 'none', width: 'auto',
'& .MuiToolbar-root': { paddingLeft: 0, paddingRight: 0, },
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
/>
</Box>
<Box sx={{
display: 'flex', alignItems: 'center', fontWeight: 'bold',
whiteSpace: 'nowrap', overflowX: 'auto', px:1, flexShrink: 1,
}}>
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>Totales</Typography> {/* Cambiado de TOTALES: a Totales */}
<Typography variant="subtitle2" sx={getCellStyle('totalPaginasImpresas')}>{numberLocaleFormatter(totalesGenerales.totalPaginasImpresas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('cantidadTiradas')}>{numberLocaleFormatter(totalesGenerales.cantidadTiradas)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('totalPaginasEjemplares')}>{numberLocaleFormatter(totalesGenerales.totalPaginasEjemplares)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('totalEjemplares')}>{numberLocaleFormatter(totalesGenerales.totalEjemplares)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle('promedioPaginasPorEjemplar')}>{numberLocaleFormatter(totalesGenerales.promedioPaginasPorEjemplar)}</Typography>
</Box>
</GridFooterContainer>
);
};
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -156,7 +260,7 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Tiradas por Publicación y Secciones</Typography>
<Typography variant="h5">Reporte: Tiradas por Publicación Mensual</Typography> {/* Título ajustado al PDF */}
<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"}
@@ -169,39 +273,35 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
</Button>
</Box>
</Box>
<Typography variant="subtitle1" gutterBottom>
Publicación: {currentParams?.nombrePublicacion} |
Planta: {currentParams?.nombrePlanta} |
Mes Consultado: {currentParams?.mesAnioParaNombreArchivo}
</Typography>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Nombre Sección</TableCell>
<TableCell align="right">Total Páginas Imp.</TableCell>
<TableCell align="right">Cant. Ediciones</TableCell>
<TableCell align="right">Total Pág. x Edición</TableCell>
<TableCell align="right">Total Ejemplares</TableCell>
<TableCell align="right">Prom. Pág./Ejemplar</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reportData.map((row, idx) => (
<TableRow key={`${row.nombreSeccion}-${idx}`}>
<TableCell>{row.nombreSeccion}</TableCell>
<TableCell align="right">{row.totalPaginasImpresas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.cantidadTiradas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.totalPaginasEjemplares.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.totalEjemplares.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.promedioPaginasPorEjemplar.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Paper sx={{ width: '100%', mt: 2 }}>
<DataGrid
rows={rows}
columns={columns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
slots={{ footer: CustomFooter }}
density="compact"
sx={{ height: 'calc(100vh - 320px)' }} // Ajustar altura según sea necesario
initialState={{
pagination: {
paginationModel: { pageSize: 100, page: 0 },
},
}}
pageSizeOptions={[25, 50 , 100]}
disableRowSelectionOnClick
/>
</Paper>
)}
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box>
);
};

View File

@@ -240,13 +240,14 @@ const ReporteVentaMensualSecretariaPage: React.FC = () => {
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell rowSpan={2} sx={{verticalAlign: 'bottom'}}>Día</TableCell>
<TableCell rowSpan={1} sx={{verticalAlign: 'bottom'}}></TableCell>
<TableCell colSpan={4} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>El Día</TableCell>
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Popular</TableCell>
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Clarín</TableCell>
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Nación</TableCell>
</TableRow>
<TableRow>
<TableCell align="right">Día</TableCell>
<TableCell align="right">Tir. Coop.</TableCell><TableCell align="right">Dev. Coop.</TableCell>
<TableCell align="right">Vta. Coop.</TableCell><TableCell align="right">Vta. Can.</TableCell>
<TableCell align="right">Tirada</TableCell><TableCell align="right">Devol.</TableCell><TableCell align="right">Venta</TableCell>

View File

@@ -1,98 +1,243 @@
import React, { useState, useEffect } from 'react';
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
import React, { useState, useEffect, useMemo } from 'react';
import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, CircularProgress } from '@mui/material';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
const reportesSubModules = [
{ label: 'Existencia de Papel', path: 'existencia-papel' },
{ 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' },
{ label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
{ label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
{ label: 'Det. Distribución Canillas', path: 'detalle-distribucion-canillas' },
{ label: 'Tiradas Pub./Sección', path: 'tiradas-publicaciones-secciones' },
{ label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
{ label: 'Consumo Bobinas/Pub.', path: 'consumo-bobinas-publicacion' },
{ label: 'Comparativa Cons. Bobinas', path: 'comparativa-consumo-bobinas' },
{ label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
// Definición de los módulos de reporte con sus categorías, etiquetas y rutas
const allReportModules: { category: string; label: string; path: string }[] = [
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' },
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' },
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' },
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' },
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' },
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' },
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' },
];
const predefinedCategoryOrder = [
'Balance de Cuentas',
'Listados Distribución',
'Ctrl. Devoluciones',
'Existencia Papel',
'Movimientos Bobinas',
'Consumos Bobinas',
'Tiradas por Publicación',
'Secretaría',
];
const ReportesIndexPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []);
useEffect(() => {
const currentBasePath = '/reportes';
// Extrae la parte de la ruta que sigue a '/reportes/'
const subPathSegment = location.pathname.startsWith(currentBasePath + '/')
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Toma solo el primer segmento
: undefined;
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
const subPathSegment = pathParts[0];
let activeTabIndex = -1;
let activeReportFoundInEffect = false;
if (subPathSegment) {
activeTabIndex = reportesSubModules.findIndex(
(subModule) => subModule.path === subPathSegment
);
}
if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
// Si estamos exactamente en '/reportes' y hay sub-módulos, navegar al primero.
if (location.pathname === currentBasePath && reportesSubModules.length > 0) {
navigate(reportesSubModules[0].path, { replace: true }); // Navega a la sub-ruta
// setSelectedSubTab(0); // Esto se manejará en la siguiente ejecución del useEffect debido al cambio de ruta
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
const activeReport = allReportModules.find(module => module.path === subPathSegment);
if (activeReport) {
setExpandedCategory(activeReport.category);
activeReportFoundInEffect = true;
} else {
setSelectedSubTab(false); // Ninguna sub-ruta activa o conocida, o no hay sub-módulos
setExpandedCategory(false);
}
} else {
setExpandedCategory(false);
}
if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) {
let firstReportToNavigate: { category: string; label: string; path: string } | null = null;
for (const category of uniqueCategories) {
const reportsInCat = allReportModules.filter(r => r.category === category);
if (reportsInCat.length > 0) {
firstReportToNavigate = reportsInCat[0];
break;
}
}
if (firstReportToNavigate) {
navigate(firstReportToNavigate.path, { replace: true });
activeReportFoundInEffect = true;
}
}
// Solo se establece a false si no estamos en el proceso de navegación inicial O si no se encontró reporte
if (!activeReportFoundInEffect || location.pathname !== currentBasePath) {
setIsLoadingInitialNavigation(false);
}
}, [location.pathname, navigate]); // Solo depende de location.pathname y navigate
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
// No es necesario setSelectedSubTab aquí directamente, el useEffect lo manejará.
navigate(reportesSubModules[newValue].path);
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]);
const handleCategoryClick = (categoryName: string) => {
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
};
// Si no hay sub-módulos definidos, podría ser un estado inicial
if (reportesSubModules.length === 0) {
const handleReportClick = (reportPath: string) => {
navigate(reportPath);
};
const isReportActive = (reportPath: string) => {
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`);
};
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
return (
<Box sx={{ p: 2 }}>
<Typography variant="h5" gutterBottom>Módulo de Reportes</Typography>
<Typography>No hay reportes configurados.</Typography>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
<CircularProgress />
</Box>
);
}
return (
<Box>
<Typography variant="h5" gutterBottom>
Módulo de Reportes
</Typography>
<Paper square elevation={1}>
<Tabs
value={selectedSubTab} // 'false' es un valor válido para Tabs si ninguna pestaña está seleccionada
onChange={handleSubTabChange}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
scrollButtons="auto"
aria-label="sub-módulos de reportes"
// Contenedor principal que se adaptará a su padre
// Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
{/* Panel Lateral para Navegación */}
<Paper
elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
square // Bordes rectos
sx={{
width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
minWidth: { xs: 200, sm: 220 },
height: '100%', // Ocupa toda la altura del Box padre
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
overflowY: 'auto',
bgcolor: 'background.paper', // O el color que desees para el menú
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical
}}
>
{/* Título del Menú Lateral */}
<Box
sx={{
p: 1.5, // Padding interno para el título
// borderBottom: (theme) => `1px solid ${theme.palette.divider}`, // Opcional: separador
// position: 'sticky', // Si quieres que el título quede fijo al hacer scroll en la lista
// top: 0,
// zIndex: 1,
// bgcolor: 'background.paper' // Necesario si es sticky y tiene scroll la lista
}}
>
{reportesSubModules.map((subModule) => (
<Tab key={subModule.path} label={subModule.label} />
))}
</Tabs>
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
Reportes
</Typography>
</Box>
{/* Lista de Categorías y Reportes */}
{uniqueCategories.length > 0 ? (
<List component="nav" dense sx={{ pt: 0 }} /* Quitar padding superior de la lista si el título ya lo tiene */ >
{uniqueCategories.map((category) => {
const reportsInCategory = allReportModules.filter(r => r.category === category);
const isExpanded = expandedCategory === category;
return (
<React.Fragment key={category}>
<ListItemButton
onClick={() => handleCategoryClick(category)}
sx={{
// py: 1.2, // Ajustar padding vertical de items de categoría
// backgroundColor: isExpanded ? 'action.selected' : 'transparent',
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander
'&:hover': {
backgroundColor: 'action.hover'
}
}}
>
<ListItemText
primary={category}
primaryTypographyProps={{
fontWeight: isExpanded ? 'bold' : 'normal',
// color: isExpanded ? 'primary.main' : 'text.primary'
}}
/>
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
</ListItemButton>
{reportsInCategory.length > 0 && (
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List component="div" disablePadding dense>
{reportsInCategory.map((report) => (
<ListItemButton
key={report.path}
selected={isReportActive(report.path)}
onClick={() => handleReportClick(report.path)}
sx={{
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título)
py: 0.8, // Padding vertical de items de reporte
...(isReportActive(report.path) && {
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo
'& .MuiListItemText-primary': {
fontWeight: 'medium', // O 'bold'
// color: 'primary.main'
},
}),
'&:hover': {
backgroundColor: (theme) => theme.palette.action.hover
}
}}
>
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
</ListItemButton>
))}
</List>
</Collapse>
)}
{reportsInCategory.length === 0 && isExpanded && (
<ListItemText
primary="No hay reportes en esta categoría."
sx={{ pl: 3.5, fontStyle: 'italic', color: 'text.secondary', py:1, typography: 'body2' }}
/>
)}
</React.Fragment>
);
})}
</List>
) : (
<Typography sx={{p:2, fontStyle: 'italic'}}>No hay categorías configuradas.</Typography>
)}
</Paper>
<Box sx={{ pt: 2 }}>
{/* Outlet renderizará ReporteExistenciaPapelPage u otros
Solo renderiza el Outlet si hay una pestaña seleccionada VÁLIDA.
Si selectedSubTab es 'false' (porque ninguna ruta coincide con los sub-módulos),
se muestra el mensaje.
*/}
{selectedSubTab !== false ? <Outlet /> : <Typography sx={{p:2}}>Seleccione un reporte del menú lateral o de las pestañas.</Typography>}
{/* Área Principal para el Contenido del Reporte */}
<Box
component="main"
sx={{
flexGrow: 1, // Ocupa el espacio restante
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo
overflowY: 'auto',
height: '100%', // Ocupa toda la altura del Box padre
bgcolor: 'grey.100' // Un color de fondo diferente para distinguir el área de contenido
}}
>
{/* El Outlet renderiza el componente del reporte específico */}
{(!location.pathname.startsWith('/reportes/') || !allReportModules.some(r => isReportActive(r.path))) && location.pathname !== '/reportes/' && location.pathname !== '/reportes' && !isLoadingInitialNavigation && (
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
El reporte solicitado no existe o la ruta no es válida.
</Typography>
)}
{(location.pathname === '/reportes/' || location.pathname === '/reportes') && !allReportModules.some(r => isReportActive(r.path)) && !isLoadingInitialNavigation && (
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."}
</Typography>
)}
<Outlet />
</Box>
</Box>
);

View File

@@ -0,0 +1,112 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem
} from '@mui/material';
import empresaService from '../../services/Distribucion/empresaService';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
interface SeleccionaReporteControlDevolucionesProps {
onGenerarReporte: (params: {
fecha: string;
idEmpresa: number;
}) => Promise<void>;
onCancel?: () => void; // Hacer onCancel opcional si no siempre se usa
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteControlDevoluciones: React.FC<SeleccionaReporteControlDevolucionesProps> = ({
onGenerarReporte,
onCancel,
isLoading,
apiErrorMessage
}) => {
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
useEffect(() => {
const fetchEmpresas = async () => {
setLoadingEmpresas(true);
try {
const data = await empresaService.getAllEmpresas(); // Solo habilitadas
setEmpresas(data);
} catch (error) {
console.error("Error al cargar empresas:", error);
setLocalError('Error al cargar empresas.');
} finally {
setLoadingEmpresas(false);
}
};
fetchEmpresas();
}, []);
const validate = (): boolean => {
if (!fecha) {
setLocalError('Debe seleccionar una fecha.');
return false;
}
if (!idEmpresa) {
setLocalError('Debe seleccionar una empresa.');
return false;
}
setLocalError(null);
return true;
};
const handleGenerar = () => {
if (!validate()) return;
onGenerarReporte({
fecha,
idEmpresa: Number(idEmpresa)
});
};
return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
<Typography variant="h6" gutterBottom>
Parámetros: Control de Devoluciones
</Typography>
<TextField
label="Fecha"
type="date"
value={fecha}
onChange={(e) => setFecha(e.target.value)}
margin="normal"
fullWidth
required
disabled={isLoading}
InputLabelProps={{ shrink: true }}
/>
<FormControl fullWidth margin="normal" error={!idEmpresa && !!localError} disabled={isLoading || loadingEmpresas}>
<InputLabel id="empresa-select-label" required>Empresa</InputLabel>
<Select
labelId="empresa-select-label"
label="Empresa"
value={idEmpresa}
onChange={(e) => setIdEmpresa(e.target.value as number)}
>
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
{empresas.map((emp) => (
<MenuItem key={emp.idEmpresa} value={emp.idEmpresa}>{emp.nombre}</MenuItem>
))}
</Select>
</FormControl>
{(localError && !apiErrorMessage) && <Alert severity="warning" sx={{ mt: 2 }}>{localError}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
{onCancel && <Button onClick={onCancel} variant="outlined" color="secondary" disabled={isLoading}>Cancelar</Button>}
<Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingEmpresas}>
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
</Button>
</Box>
</Box>
);
};
export default SeleccionaReporteControlDevoluciones;

View File

@@ -0,0 +1,157 @@
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';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
import distribuidorService from '../../services/Distribucion/distribuidorService';
interface SeleccionaReporteListadoDistribucionProps {
onGenerarReporte: (params: {
idDistribuidor: number;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}) => Promise<void>;
onCancel: () => void;
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteListadoDistribucion: React.FC<SeleccionaReporteListadoDistribucionProps> = ({
onGenerarReporte,
isLoading,
apiErrorMessage
}) => {
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
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 [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => {
const fetchData = async () => {
setLoadingDropdowns(true);
try {
const [distData, pubData] = await Promise.all([
distribuidorService.getAllDistribuidores(),
publicacionService.getAllPublicaciones(undefined, undefined, true) // Solo habilitadas
]);
setDistribuidores(distData.map(d => d));
setPublicaciones(pubData.map(p => p));
} catch (error) {
console.error("Error al cargar datos:", error);
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos para los selectores.' }));
} finally {
setLoadingDropdowns(false);
}
};
fetchData();
}, []);
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
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({
idDistribuidor: Number(idDistribuidor),
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 (Distribuidores)
</Typography>
<FormControl fullWidth margin="normal" error={!!localErrors.idDistribuidor} disabled={isLoading || loadingDropdowns}>
<InputLabel id="distribuidor-select-label-dist" required>Distribuidor</InputLabel>
<Select
labelId="distribuidor-select-label-dist"
label="Distribuidor"
value={idDistribuidor}
onChange={(e) => { setIdDistribuidor(e.target.value as number); setLocalErrors(p => ({ ...p, idDistribuidor: null })); }}
>
<MenuItem value="" disabled><em>Seleccione un distribuidor</em></MenuItem>
{distribuidores.map((d) => (
<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>
))}
</Select>
{localErrors.idDistribuidor && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idDistribuidor}</Typography>}
</FormControl>
<FormControl fullWidth margin="normal" error={!!localErrors.idPublicacion} disabled={isLoading || loadingDropdowns}>
<InputLabel id="publicacion-select-label-dist" required>Publicación</InputLabel>
<Select
labelId="publicacion-select-label-dist"
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 SeleccionaReporteListadoDistribucion;

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
FormControl, InputLabel, Select, MenuItem,
ToggleButtonGroup,
ToggleButton
} from '@mui/material';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import publicacionService from '../../services/Distribucion/publicacionService';
@@ -116,17 +118,65 @@ const SeleccionaReporteListadoDistribucionCanillasImporte: React.FC<SeleccionaRe
disabled={isLoading}
InputLabelProps={{ shrink: true }}
/>
<FormControlLabel
control={
<Checkbox
checked={esAccionista}
onChange={(e) => setEsAccionista(e.target.checked)}
disabled={isLoading}
/>
}
label="Ver Accionistas"
sx={{ mt: 1, mb: 1 }}
/>
<Box sx={{ mt: 2, mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
Tipo de reporte
</Typography>
<ToggleButtonGroup
value={esAccionista ? 'accionistas' : 'canillitas'}
exclusive
onChange={(_, value) => {
if (value !== null) setEsAccionista(value === 'accionistas');
}}
aria-label="Tipo de reporte"
disabled={isLoading}
color="primary"
size="large"
sx={{
borderRadius: 2,
boxShadow: 2,
backgroundColor: '#f5f5f5',
p: 0.5,
}}
>
<ToggleButton
value="canillitas"
aria-label="Canillitas"
sx={{
fontWeight: esAccionista ? 400 : 700,
bgcolor: !esAccionista ? 'primary.main' : 'background.paper',
color: !esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Canillitas
</ToggleButton>
<ToggleButton
value="accionistas"
aria-label="Accionistas"
sx={{
fontWeight: esAccionista ? 700 : 400,
bgcolor: esAccionista ? 'primary.main' : 'background.paper',
color: esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Accionistas
</ToggleButton>
</ToggleButtonGroup>
</Box>
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}

View File

@@ -67,6 +67,8 @@ import ReporteConsumoBobinasSeccionPage from '../pages/Reportes/ReporteConsumoBo
import ReporteConsumoBobinasPublicacionPage from '../pages/Reportes/ReporteConsumoBobinasPublicacionPage';
import ReporteComparativaConsumoBobinasPage from '../pages/Reportes/ReporteComparativaConsumoBobinasPage';
import ReporteCuentasDistribuidoresPage from '../pages/Reportes/ReporteCuentasDistribuidoresPage';
import ReporteListadoDistribucionPage from '../pages/Reportes/ReporteListadoDistribucionPage';
import ReporteControlDevolucionesPage from '../pages/Reportes/ReporteControlDevolucionesPage';
// Auditorias
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
@@ -177,6 +179,8 @@ const AppRoutes = () => {
<Route path="consumo-bobinas-publicacion" element={<ReporteConsumoBobinasPublicacionPage />} />
<Route path="comparativa-consumo-bobinas" element={<ReporteComparativaConsumoBobinasPage />} />
<Route path="cuentas-distribuidores" element={<ReporteCuentasDistribuidoresPage />} />
<Route path="listado-distribucion-distribuidores" element={<ReporteListadoDistribucionPage />} />
<Route path="control-devoluciones" element={<ReporteControlDevolucionesPage />} />
</Route>
{/* Módulo de Radios (anidado) */}

View File

@@ -14,345 +14,393 @@ import type { ConsumoBobinasSeccionDto } from '../../models/dtos/Reportes/Consum
import type { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto';
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto';
import type { ListadoDistribucionDistribuidoresResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionDistribuidoresResponseDto';
import type { ControlDevolucionesDataResponseDto } from '../../models/dtos/Reportes/ControlDevolucionesDataResponseDto';
interface GetExistenciaPapelParams {
fechaDesde: string; // yyyy-MM-dd
fechaHasta: string; // yyyy-MM-dd
idPlanta?: number | null;
consolidado: boolean;
fechaDesde: string; // yyyy-MM-dd
fechaHasta: string; // yyyy-MM-dd
idPlanta?: number | null;
consolidado: boolean;
}
const getExistenciaPapelPdf = async (params: GetExistenciaPapelParams): Promise<Blob> => {
const queryParams: Record<string, string | number | boolean> = {
fechaDesde: params.fechaDesde,
fechaHasta: params.fechaHasta,
consolidado: params.consolidado,
};
if (params.idPlanta && !params.consolidado) {
queryParams.idPlanta = params.idPlanta;
}
const queryParams: Record<string, string | number | boolean> = {
fechaDesde: params.fechaDesde,
fechaHasta: params.fechaHasta,
consolidado: params.consolidado,
};
if (params.idPlanta && !params.consolidado) {
queryParams.idPlanta = params.idPlanta;
}
const response = await apiClient.get('/reportes/existencia-papel/pdf', {
params: queryParams,
responseType: 'blob', // ¡Importante para descargar archivos!
});
return response.data; // response.data será un Blob
const response = await apiClient.get('/reportes/existencia-papel/pdf', {
params: queryParams,
responseType: 'blob', // ¡Importante para descargar archivos!
});
return response.data; // response.data será un Blob
};
const getExistenciaPapel = async (params: GetExistenciaPapelParams): Promise<ExistenciaPapelDto[]> => {
// Construir los query params, omitiendo idPlanta si es consolidado o no está definido
const queryParams: Record<string, string | number | boolean> = {
fechaDesde: params.fechaDesde,
fechaHasta: params.fechaHasta,
consolidado: params.consolidado,
};
if (params.idPlanta && !params.consolidado) {
queryParams.idPlanta = params.idPlanta;
}
// Construir los query params, omitiendo idPlanta si es consolidado o no está definido
const queryParams: Record<string, string | number | boolean> = {
fechaDesde: params.fechaDesde,
fechaHasta: params.fechaHasta,
consolidado: params.consolidado,
};
if (params.idPlanta && !params.consolidado) {
queryParams.idPlanta = params.idPlanta;
}
const response = await apiClient.get<ExistenciaPapelDto[]>('/reportes/existencia-papel', { params: queryParams });
return response.data;
const response = await apiClient.get<ExistenciaPapelDto[]>('/reportes/existencia-papel', { params: queryParams });
return response.data;
};
const getMovimientoBobinas = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}): Promise<MovimientoBobinasDto[]> => {
const response = await apiClient.get<MovimientoBobinasDto[]>('/reportes/movimiento-bobinas', { params });
return response.data;
const response = await apiClient.get<MovimientoBobinasDto[]>('/reportes/movimiento-bobinas', { params });
return response.data;
};
const getMovimientoBobinasPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/movimiento-bobinas/pdf', {
params,
responseType: 'blob',
});
return response.data;
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;
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 response = await apiClient.get<MovimientoBobinasPorEstadoResponseDto>('/reportes/movimiento-bobinas-estado', { params });
return response.data;
};
const getMovimientoBobinasEstadoPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta: number;
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 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)
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 response = await apiClient.get<ListadoDistribucionGeneralResponseDto>('/reportes/listado-distribucion-general', { params });
return response.data;
};
const getListadoDistribucionGeneralPdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
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 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;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<ListadoDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ListadoDistribucionCanillasResponseDto>('/reportes/listado-distribucion-canillas', { params });
return response.data;
const response = await apiClient.get<ListadoDistribucionCanillasResponseDto>('/reportes/listado-distribucion-canillas', { params });
return response.data;
};
const getListadoDistribucionCanillasPdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
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 response = await apiClient.get('/reportes/listado-distribucion-canillas/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getListadoDistribucionCanillasImporte = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
esAccionista: boolean;
}): Promise<ListadoDistribucionCanillasImporteDto[]> => {
const response = await apiClient.get<ListadoDistribucionCanillasImporteDto[]>('/reportes/listado-distribucion-canillas-importe', { params });
return response.data;
};
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
esAccionista: boolean;
}): Promise<ListadoDistribucionCanillasImporteDto[]> => {
const response = await apiClient.get<ListadoDistribucionCanillasImporteDto[]>('/reportes/listado-distribucion-canillas-importe', { params });
return response.data;
};
const getListadoDistribucionCanillasImportePdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
esAccionista: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-canillas-importe/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getListadoDistribucionCanillasImportePdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
esAccionista: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-canillas-importe/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getVentaMensualSecretariaElDia = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElDiaDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaElDiaDto[]>('/reportes/venta-mensual-secretaria/el-dia', { params });
return response.data;
};
const getVentaMensualSecretariaElDia = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElDiaDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaElDiaDto[]>('/reportes/venta-mensual-secretaria/el-dia', { params });
return response.data;
};
const getVentaMensualSecretariaElDiaPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => {
const response = await apiClient.get('/reportes/venta-mensual-secretaria/el-dia/pdf', { params, responseType: 'blob' });
return response.data;
};
const getVentaMensualSecretariaElDiaPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => {
const response = await apiClient.get('/reportes/venta-mensual-secretaria/el-dia/pdf', { params, responseType: 'blob' });
return response.data;
};
const getVentaMensualSecretariaElPlata = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElPlataDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaElPlataDto[]>('/reportes/venta-mensual-secretaria/el-plata', { params });
return response.data;
};
const getVentaMensualSecretariaElPlata = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElPlataDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaElPlataDto[]>('/reportes/venta-mensual-secretaria/el-plata', { params });
return response.data;
};
const getVentaMensualSecretariaElPlataPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => {
const response = await apiClient.get('/reportes/venta-mensual-secretaria/el-plata/pdf', { params, responseType: 'blob' });
return response.data;
};
const getVentaMensualSecretariaElPlataPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => {
const response = await apiClient.get('/reportes/venta-mensual-secretaria/el-plata/pdf', { params, responseType: 'blob' });
return response.data;
};
const getVentaMensualSecretariaTirDevo = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaTirDevoDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaTirDevoDto[]>('/reportes/venta-mensual-secretaria/tirada-devolucion', { params });
return response.data;
};
const getVentaMensualSecretariaTirDevoPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => {
const response = await apiClient.get('/reportes/venta-mensual-secretaria/tirada-devolucion/pdf', { params, responseType: 'blob' });
return response.data;
};
const getVentaMensualSecretariaTirDevo = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaTirDevoDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaTirDevoDto[]>('/reportes/venta-mensual-secretaria/tirada-devolucion', { params });
return response.data;
};
const getVentaMensualSecretariaTirDevoPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => {
const response = await apiClient.get('/reportes/venta-mensual-secretaria/tirada-devolucion/pdf', { params, responseType: 'blob' });
return response.data;
};
const getReporteDistribucionCanillas = async (params: {
fecha: string;
idEmpresa: number;
}): Promise<ReporteDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params });
return response.data;
};
const getReporteDistribucionCanillas = async (params: {
fecha: string;
idEmpresa: number;
}): Promise<ReporteDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params });
return response.data;
};
const getReporteDistribucionCanillasPdf = async (params: {
fecha: string;
idEmpresa: number;
soloTotales: boolean; // Nuevo parámetro
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { // La ruta no necesita cambiar si el backend lo maneja
params, // soloTotales se enviará como query param si el backend lo espera así
responseType: 'blob',
});
return response.data;
};
const getReporteDistribucionCanillasPdf = async (params: {
fecha: string;
idEmpresa: number;
soloTotales: boolean; // Nuevo parámetro
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { // La ruta no necesita cambiar si el backend lo maneja
params, // soloTotales se enviará como query param si el backend lo espera así
responseType: 'blob',
});
return response.data;
};
const getTiradasPublicacionesSecciones = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<TiradasPublicacionesSeccionesDto[]> => {
const response = await apiClient.get<TiradasPublicacionesSeccionesDto[]>('/reportes/tiradas-publicaciones-secciones', { params });
return response.data;
};
const getTiradasPublicacionesSecciones = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<TiradasPublicacionesSeccionesDto[]> => {
const response = await apiClient.get<TiradasPublicacionesSeccionesDto[]>('/reportes/tiradas-publicaciones-secciones', { params });
return response.data;
};
const getTiradasPublicacionesSeccionesPdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/tiradas-publicaciones-secciones/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getTiradasPublicacionesSeccionesPdf = async (params: {
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/tiradas-publicaciones-secciones/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getConsumoBobinasSeccion = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<ConsumoBobinasSeccionDto[]> => {
const response = await apiClient.get<ConsumoBobinasSeccionDto[]>('/reportes/consumo-bobinas-seccion', { params });
return response.data;
};
const getConsumoBobinasSeccion = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<ConsumoBobinasSeccionDto[]> => {
const response = await apiClient.get<ConsumoBobinasSeccionDto[]>('/reportes/consumo-bobinas-seccion', { params });
return response.data;
};
const getConsumoBobinasSeccionPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/consumo-bobinas-seccion/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getConsumoBobinasSeccionPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/consumo-bobinas-seccion/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getConsumoBobinasPorPublicacion = async (params: {
fechaDesde: string;
fechaHasta: string;
}): Promise<ConsumoBobinasPublicacionDto[]> => {
const response = await apiClient.get<ConsumoBobinasPublicacionDto[]>('/reportes/consumo-bobinas-publicacion', { params });
return response.data;
};
const getConsumoBobinasPorPublicacion = async (params: {
fechaDesde: string;
fechaHasta: string;
}): Promise<ConsumoBobinasPublicacionDto[]> => {
const response = await apiClient.get<ConsumoBobinasPublicacionDto[]>('/reportes/consumo-bobinas-publicacion', { params });
return response.data;
};
const getConsumoBobinasPorPublicacionPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/consumo-bobinas-publicacion/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getConsumoBobinasPorPublicacionPdf = async (params: {
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/consumo-bobinas-publicacion/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getComparativaConsumoBobinas = async (params: {
fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
}): Promise<ComparativaConsumoBobinasDto[]> => {
const response = await apiClient.get<ComparativaConsumoBobinasDto[]>('/reportes/comparativa-consumo-bobinas', { params });
return response.data;
};
const getComparativaConsumoBobinas = async (params: {
fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
}): Promise<ComparativaConsumoBobinasDto[]> => {
const response = await apiClient.get<ComparativaConsumoBobinasDto[]>('/reportes/comparativa-consumo-bobinas', { params });
return response.data;
};
const getComparativaConsumoBobinasPdf = async (params: {
fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/comparativa-consumo-bobinas/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getComparativaConsumoBobinasPdf = async (params: {
fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/comparativa-consumo-bobinas/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getReporteCuentasDistribuidor = async (params: {
idDistribuidor: number;
idEmpresa: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<ReporteCuentasDistribuidorResponseDto> => {
const response = await apiClient.get<ReporteCuentasDistribuidorResponseDto>('/reportes/cuentas-distribuidores', { params });
return response.data;
};
const getReporteCuentasDistribuidor = async (params: {
idDistribuidor: number;
idEmpresa: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<ReporteCuentasDistribuidorResponseDto> => {
const response = await apiClient.get<ReporteCuentasDistribuidorResponseDto>('/reportes/cuentas-distribuidores', { params });
return response.data;
};
const getReporteCuentasDistribuidorPdf = async (params: {
idDistribuidor: number;
idEmpresa: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/cuentas-distribuidores/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getReporteCuentasDistribuidorPdf = async (params: {
idDistribuidor: number;
idEmpresa: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/cuentas-distribuidores/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getListadoDistribucionDistribuidores = async (params: {
idDistribuidor: number;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<ListadoDistribucionDistribuidoresResponseDto> => {
const response = await apiClient.get<ListadoDistribucionDistribuidoresResponseDto>('/reportes/listado-distribucion-distribuidores', { params });
return response.data;
};
const getListadoDistribucionDistribuidoresPdf = async (params: {
idDistribuidor: number;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-distribuidores/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const getControlDevolucionesData = async (params: {
fecha: string; // YYYY-MM-DD
idEmpresa: number;
}): Promise<ControlDevolucionesDataResponseDto> => {
const response = await apiClient.get<ControlDevolucionesDataResponseDto>('/reportes/control-devoluciones', { params });
return response.data;
};
const getControlDevolucionesPdf = async (params: {
fecha: string; // YYYY-MM-DD
idEmpresa: number;
}): Promise<Blob> => {
const response = await apiClient.get('/reportes/control-devoluciones/pdf', {
params,
responseType: 'blob',
});
return response.data;
};
const reportesService = {
getExistenciaPapel,
getExistenciaPapelPdf,
getMovimientoBobinas,
getMovimientoBobinasPdf,
getMovimientoBobinasEstado,
getMovimientoBobinasEstadoPdf,
getListadoDistribucionGeneral,
getListadoDistribucionGeneralPdf,
getListadoDistribucionCanillas,
getListadoDistribucionCanillasPdf,
getListadoDistribucionCanillasImporte,
getListadoDistribucionCanillasImportePdf,
getVentaMensualSecretariaElDia,
getVentaMensualSecretariaElDiaPdf,
getVentaMensualSecretariaElPlata,
getVentaMensualSecretariaElPlataPdf,
getVentaMensualSecretariaTirDevo,
getVentaMensualSecretariaTirDevoPdf,
getReporteDistribucionCanillas,
getReporteDistribucionCanillasPdf,
getTiradasPublicacionesSecciones,
getTiradasPublicacionesSeccionesPdf,
getConsumoBobinasSeccion,
getConsumoBobinasSeccionPdf,
getConsumoBobinasPorPublicacion,
getConsumoBobinasPorPublicacionPdf,
getComparativaConsumoBobinas,
getComparativaConsumoBobinasPdf,
getReporteCuentasDistribuidor,
getReporteCuentasDistribuidorPdf,
getExistenciaPapel,
getExistenciaPapelPdf,
getMovimientoBobinas,
getMovimientoBobinasPdf,
getMovimientoBobinasEstado,
getMovimientoBobinasEstadoPdf,
getListadoDistribucionGeneral,
getListadoDistribucionGeneralPdf,
getListadoDistribucionCanillas,
getListadoDistribucionCanillasPdf,
getListadoDistribucionCanillasImporte,
getListadoDistribucionCanillasImportePdf,
getVentaMensualSecretariaElDia,
getVentaMensualSecretariaElDiaPdf,
getVentaMensualSecretariaElPlata,
getVentaMensualSecretariaElPlataPdf,
getVentaMensualSecretariaTirDevo,
getVentaMensualSecretariaTirDevoPdf,
getReporteDistribucionCanillas,
getReporteDistribucionCanillasPdf,
getTiradasPublicacionesSecciones,
getTiradasPublicacionesSeccionesPdf,
getConsumoBobinasSeccion,
getConsumoBobinasSeccionPdf,
getConsumoBobinasPorPublicacion,
getConsumoBobinasPorPublicacionPdf,
getComparativaConsumoBobinas,
getComparativaConsumoBobinasPdf,
getReporteCuentasDistribuidor,
getReporteCuentasDistribuidorPdf,
getListadoDistribucionDistribuidores,
getListadoDistribucionDistribuidoresPdf,
getControlDevolucionesData,
getControlDevolucionesPdf,
};
export default reportesService;