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

@@ -127,15 +127,19 @@ namespace GestionIntegral.Api.Controllers
{ {
var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value); var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
nombrePlantaParam = planta?.Nombre ?? "N/A"; 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"; nombrePlantaParam = "N/A";
} }
// Solo añadir NomPlanta si NO es consolidado, porque el RDLC consolidado no lo tiene. // Solo añadir NomPlanta si NO es consolidado, porque el RDLC consolidado no lo tiene.
if (!consolidado) { if (!consolidado)
parameters.Add(new ReportParameter("NomPlanta", nombrePlantaParam)); {
parameters.Add(new ReportParameter("NomPlanta", nombrePlantaParam));
} }
@@ -236,7 +240,7 @@ namespace GestionIntegral.Api.Controllers
if ((detalle == null || !detalle.Any()) && (totales == null || !totales.Any())) 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 var response = new MovimientoBobinasPorEstadoResponseDto
@@ -265,7 +269,7 @@ namespace GestionIntegral.Api.Controllers
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
if ((detalle == null || !detalle.Any()) && (totales == null || !totales.Any())) 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 try
@@ -506,7 +510,7 @@ namespace GestionIntegral.Api.Controllers
try try
{ {
LocalReport report = new LocalReport(); 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); report.LoadReportDefinition(fs);
} }
@@ -606,7 +610,7 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs); report.LoadReportDefinition(fs);
} }
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", data)); // Basado en el RDLC 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("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")) new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
}); });
@@ -647,7 +651,7 @@ namespace GestionIntegral.Api.Controllers
report.LoadReportDefinition(fs); report.LoadReportDefinition(fs);
} }
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", data)); // Basado en el RDLC 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("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")) new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
}); });
@@ -713,7 +717,7 @@ namespace GestionIntegral.Api.Controllers
canillasAccFechaLiq, canillasAccFechaLiq,
ctrlDevolucionesRemitos, // Renombrado para claridad ctrlDevolucionesRemitos, // Renombrado para claridad
ctrlDevolucionesParaDistCan, ctrlDevolucionesParaDistCan,
_ , // Descartamos ctrlDevolucionesOtrosDias si no se usa aquí _, // Descartamos ctrlDevolucionesOtrosDias si no se usa aquí
error error
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
@@ -818,7 +822,7 @@ namespace GestionIntegral.Api.Controllers
[HttpGet("control-devoluciones/pdf")] [HttpGet("control-devoluciones/pdf")]
public async Task<IActionResult> GetReporteControlDevolucionesPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa) 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 // La tupla ahora devuelve un campo más
var ( var (
@@ -897,7 +901,7 @@ namespace GestionIntegral.Api.Controllers
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && !saldos.Any()) 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 distribuidor = await _distribuidorRepository.GetByIdAsync(idDistribuidor);
@@ -931,7 +935,7 @@ namespace GestionIntegral.Api.Controllers
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && !saldos.Any()) 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 try
@@ -1120,9 +1124,9 @@ namespace GestionIntegral.Api.Controllers
try try
{ {
LocalReport report = new LocalReport(); LocalReport report = new LocalReport();
string rdlcPath = consolidado ? string rdlcPath = consolidado ?
"Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccionConsolidado.rdlc" : "Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccionConsolidado.rdlc" :
"Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccion.rdlc"; "Controllers/Reportes/RDLC/ReporteConsumoBobinasSeccion.rdlc";
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{ {
report.LoadReportDefinition(fs); report.LoadReportDefinition(fs);
@@ -1134,7 +1138,7 @@ namespace GestionIntegral.Api.Controllers
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")), new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
new ReportParameter("FechaHasta", fechaHasta.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); var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A")); parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A"));
@@ -1215,7 +1219,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int? idPlanta, [FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false) [FromQuery] bool consolidado = false)
{ {
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid(); if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
IEnumerable<ComparativaConsumoBobinasDto> data; IEnumerable<ComparativaConsumoBobinasDto> data;
string? errorMsg; string? errorMsg;
@@ -1243,7 +1247,7 @@ namespace GestionIntegral.Api.Controllers
[FromQuery] int? idPlanta, [FromQuery] int? idPlanta,
[FromQuery] bool consolidado = false) [FromQuery] bool consolidado = false)
{ {
if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid(); if (!TienePermiso(PermisoVerReporteConsumoBobinas)) return Forbid();
IEnumerable<ComparativaConsumoBobinasDto> data; IEnumerable<ComparativaConsumoBobinasDto> data;
string? errorMsg; string? errorMsg;
@@ -1279,7 +1283,7 @@ namespace GestionIntegral.Api.Controllers
new ReportParameter("MesA", fechaInicioMesA.ToString("MMMM yyyy")), new ReportParameter("MesA", fechaInicioMesA.ToString("MMMM yyyy")),
new ReportParameter("MesB", fechaInicioMesB.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); var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
parameters.Add(new ReportParameter("NomPlanta", planta?.Nombre ?? "N/A")); 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."); } 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<MovimientoBobinasDto>> GetMovimientoBobinasAsync(DateTime fechaInicio, int diasPeriodo, int idPlanta);
Task<IEnumerable<MovimientoBobinaEstadoDetalleDto>> GetMovimientoBobinasEstadoDetalleAsync(DateTime fechaInicio, DateTime fechaFin, int idPlanta); Task<IEnumerable<MovimientoBobinaEstadoDetalleDto>> GetMovimientoBobinasEstadoDetalleAsync(DateTime fechaInicio, DateTime fechaFin, int idPlanta);
Task<IEnumerable<MovimientoBobinaEstadoTotalDto>> GetMovimientoBobinasEstadoTotalesAsync(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<ListadoDistribucionGeneralResumenDto>> GetListadoDistribucionGeneralResumenAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<ListadoDistribucionGeneralPromedioDiaDto>> GetListadoDistribucionGeneralPromedioDiaAsync(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); 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<BalanceCuentaDebCredDto>> GetBalanceCuentDistDebCredEmpresaAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<BalanceCuentaPagosDto>> GetBalanceCuentDistPagosEmpresaAsync(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<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) public async Task<IEnumerable<ListadoDistribucionGeneralResumenDto>> GetListadoDistribucionGeneralResumenAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
{ {
const string spName = "dbo.SP_DistObtenerResumenMensual"; 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); } 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>(); } 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, IEnumerable<SaldoDto> Saldos,
string? Error string? Error
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); )> 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.AssemblyCompanyAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [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.AssemblyProductAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [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 import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual
interface MainLayoutProps { 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 = [ const modules = [
{ label: 'Inicio', path: '/' }, { label: 'Inicio', path: '/' },
{ label: 'Distribución', path: '/distribucion' }, // Asumiremos rutas base como /distribucion, /contables, etc. { label: 'Distribución', path: '/distribucion' },
{ label: 'Contables', path: '/contables' }, { label: 'Contables', path: '/contables' },
{ label: 'Impresión', path: '/impresion' }, { label: 'Impresión', path: '/impresion' },
{ label: 'Reportes', path: '/reportes' }, { label: 'Reportes', path: '/reportes' },
{ label: 'Radios', path: '/radios' }, { label: 'Radios', path: '/radios' },
{ label: 'Usuarios', path: '/usuarios' }, { label: 'Usuarios', path: '/usuarios' },
]; ];
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const { const {
user, user,
logout, logout,
showForcedPasswordChangeModal, // ... (resto de las props de useAuth) ...
isAuthenticated,
isPasswordChangeForced, isPasswordChangeForced,
passwordChangeCompleted, showForcedPasswordChangeModal,
setShowForcedPasswordChangeModal, setShowForcedPasswordChangeModal,
isAuthenticated passwordChangeCompleted
} = useAuth(); } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); // Para obtener la ruta actual const location = useLocation(); // Para obtener la ruta actual
// Estado para el tab seleccionado
const [selectedTab, setSelectedTab] = useState<number | false>(false); const [selectedTab, setSelectedTab] = useState<number | false>(false);
// Efecto para sincronizar el tab seleccionado con la ruta actual
useEffect(() => { useEffect(() => {
const currentModulePath = modules.findIndex(module => const currentModulePath = modules.findIndex(module =>
location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/')) location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/'))
@@ -44,14 +43,13 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
if (currentModulePath !== -1) { if (currentModulePath !== -1) {
setSelectedTab(currentModulePath); setSelectedTab(currentModulePath);
} else if (location.pathname === '/') { } else if (location.pathname === '/') {
setSelectedTab(0); // Seleccionar "Inicio" si es la raíz setSelectedTab(0);
} else { } else {
setSelectedTab(false); // Ningún tab coincide (podría ser una sub-ruta no principal) setSelectedTab(false);
} }
}, [location.pathname]); }, [location.pathname]);
const handleModalClose = (passwordChangedSuccessfully: boolean) => { const handleModalClose = (passwordChangedSuccessfully: boolean) => {
// ... (lógica de handleModalClose existente) ...
if (passwordChangedSuccessfully) { if (passwordChangedSuccessfully) {
passwordChangeCompleted(); passwordChangeCompleted();
} else { } else {
@@ -65,54 +63,54 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setSelectedTab(newValue); 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. // Determinar si el módulo actual es el de Reportes
// El modal se superpone. const isReportesModule = location.pathname.startsWith('/reportes');
if (showForcedPasswordChangeModal && isPasswordChangeForced) { if (showForcedPasswordChangeModal && isPasswordChangeForced) {
// ... (lógica del modal forzado sin cambios) ...
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<ChangePasswordModal <ChangePasswordModal
open={showForcedPasswordChangeModal} open={showForcedPasswordChangeModal}
onClose={handleModalClose} onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced} isFirstLogin={isPasswordChangeForced}
/> />
{/* Podrías querer un fondo o layout mínimo aquí si el modal no es pantalla completa */}
</Box> </Box>
); );
} }
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}> <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="static"> <AppBar position="static">
{/* ... (Toolbar y Tabs sin cambios) ... */}
<Toolbar> <Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Sistema de Gestión - El Día Sistema de Gestión - El Día
</Typography> </Typography>
{user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>} {user && <Typography sx={{ mr: 2 }}>Hola, {user.nombreCompleto}</Typography>}
{isAuthenticated && !isPasswordChangeForced && ( {isAuthenticated && !isPasswordChangeForced && (
<Button <Button
color="inherit" color="inherit"
onClick={() => setShowForcedPasswordChangeModal(true)} // Ahora abre el modal onClick={() => setShowForcedPasswordChangeModal(true)}
> >
Cambiar Contraseña Cambiar Contraseña
</Button> </Button>
)} )}
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button> <Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
</Toolbar> </Toolbar>
{/* Navegación Principal por Módulos */} <Paper square elevation={0} >
<Paper square elevation={0} > {/* Usamos Paper para un fondo consistente para los Tabs */}
<Tabs <Tabs
value={selectedTab} value={selectedTab}
onChange={handleTabChange} onChange={handleTabChange}
indicatorColor="secondary" // O "primary" indicatorColor="secondary"
textColor="inherit" // O "primary" / "secondary" textColor="inherit"
variant="scrollable" // Permite scroll si hay muchos tabs variant="scrollable"
scrollButtons="auto" // Muestra botones de scroll si es necesario scrollButtons="auto"
aria-label="módulos principales" 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) => ( {modules.map((module) => (
<Tab key={module.path} label={module.label} /> <Tab key={module.path} label={module.label} />
@@ -121,13 +119,15 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
</Paper> </Paper>
</AppBar> </AppBar>
{/* Contenido del Módulo (renderizado por <Outlet /> en AppRoutes) */} {/* Contenido del Módulo */}
<Box <Box
component="main" component="main"
sx={{ sx={{
flexGrow: 1, flexGrow: 1,
p: 3, // Padding py: isReportesModule ? 0 : 3, // Padding vertical condicional. Si es el módulo de Reportes, px es 0 si no 3
// overflowY: 'auto' // Si el contenido del módulo es muy largo 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} {children}
@@ -139,14 +139,11 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
</Typography> </Typography>
</Box> </Box>
{/* Modal para cambio de clave opcional (no forzado) */} <ChangePasswordModal
{/* Si showForcedPasswordChangeModal es true pero isPasswordChangeForced es false,
se mostrará aquí también. */}
<ChangePasswordModal
open={showForcedPasswordChangeModal} open={showForcedPasswordChangeModal}
onClose={handleModalClose} onClose={handleModalClose}
isFirstLogin={isPasswordChangeForced} // Esto controla el comportamiento del modal isFirstLogin={isPasswordChangeForced}
/> />
</Box> </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 { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody,
TableFooter
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto'; import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
import SeleccionaReporteComparativaConsumoBobinas from './SeleccionaReporteComparativaConsumoBobinas'; import SeleccionaReporteComparativaConsumoBobinas from './SeleccionaReporteComparativaConsumoBobinas';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; import axios from 'axios';
// Interfaz extendida para DataGrid
interface ComparativaConsumoBobinasDataGridDto extends ComparativaConsumoBobinasDto {
id: string;
}
const ReporteComparativaConsumoBobinasPage: React.FC = () => { const ReporteComparativaConsumoBobinasPage: React.FC = () => {
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDto[]>([]); const [reportData, setReportData] = useState<ComparativaConsumoBobinasDataGridDto[]>([]); // Usar tipo extendido
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false); const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -21,9 +27,14 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
fechaInicioMesA: string; fechaFinMesA: string; fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string; fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean; idPlanta?: number | null; consolidado: boolean;
nombrePlanta?: string; // Para el PDF nombrePlanta?: string;
mesA?: string;
mesB?: string;
} | null>(null); } | null>(null);
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const handleGenerarReporte = useCallback(async (params: { const handleGenerarReporte = useCallback(async (params: {
fechaInicioMesA: string; fechaFinMesA: string; fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string; fechaInicioMesB: string; fechaFinMesB: string;
@@ -40,12 +51,24 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
const plantaData = await plantaService.getPlantaById(params.idPlanta); const plantaData = await plantaService.getPlantaById(params.idPlanta);
plantaNombre = plantaData?.nombre ?? "N/A"; 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 { try {
const data = await reportesService.getComparativaConsumoBobinas(params); const data = await reportesService.getComparativaConsumoBobinas(params);
setReportData(data); const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.tipoBobina}-${index}` }));
if (data.length === 0) { setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -72,7 +95,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
alert("No hay datos para exportar."); alert("No hay datos para exportar.");
return; return;
} }
const dataToExport = reportData.map(item => ({ const dataToExport = reportData.map(({ ...rest }) => rest).map(item => ({
"Tipo Bobina": item.tipoBobina, "Tipo Bobina": item.tipoBobina,
"Cant. Mes A": item.bobinasUtilizadasMesA, "Cant. Mes A": item.bobinasUtilizadasMesA,
"Cant. Mes B": item.bobinasUtilizadasMesB, "Cant. Mes B": item.bobinasUtilizadasMesB,
@@ -82,7 +105,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
"Dif. Kg": item.diferenciaKilosUtilizados, "Dif. Kg": item.diferenciaKilosUtilizados,
})); }));
// Totales
const totales = dataToExport.reduce((acc, row) => { const totales = dataToExport.reduce((acc, row) => {
acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]); acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]);
acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]); acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]);
@@ -96,7 +118,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
"Dif. Kg": totales.difKg, "Dif. Kg": totales.difKg,
}); });
const ws = XLSX.utils.json_to_sheet(dataToExport); const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {}); const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => { ws['!cols'] = headers.map(h => {
@@ -104,7 +125,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
return { wch: maxLen + 2 }; return { wch: maxLen + 2 };
}); });
ws['!freeze'] = { xSplit: 0, ySplit: 1 }; ws['!freeze'] = { xSplit: 0, ySplit: 1 };
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "ComparativaConsumo"); XLSX.utils.book_append_sheet(wb, ws, "ComparativaConsumo");
let fileName = "ReporteComparativaConsumoBobinas"; let fileName = "ReporteComparativaConsumoBobinas";
@@ -141,6 +161,118 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <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 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Comparativa Consumo de Bobinas</Typography> <Typography variant="h5">Reporte: Comparativa Consumo de Bobinas</Typography>
<Box sx={{ display: 'flex', gap: 1 }}> <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"} {loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button> </Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={reportData.length === 0 || !!error} size="small"> <Button onClick={handleExportToExcel} variant="outlined" disabled={reportData.length === 0 || !!error} size="small">
@@ -173,51 +305,36 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && ( {!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> <Paper sx={{
<Table stickyHeader size="small"> width: '100%',
<TableHead> mt: 2,
<TableRow> '& .separator-left': {
<TableCell>Tipo Bobina</TableCell> borderLeft: (theme: Theme) => `2px solid ${theme.palette.divider}`,
<TableCell align="right">Cant. Mes A</TableCell> },
<TableCell align="right">Cant. Mes B</TableCell> }}>
<TableCell align="right">Dif. Cant.</TableCell> <DataGrid
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>Kg Mes A</TableCell> rows={rows}
<TableCell align="right">Kg Mes B</TableCell> columns={columns}
<TableCell align="right">Dif. Kg</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
</TableRow> slots={{ footer: CustomFooter }}
</TableHead> density="compact"
<TableBody> sx={{ height: 'calc(100vh - 300px)' }} // Ajusta esta altura según sea necesario
{reportData.map((row, idx) => ( initialState={{
<TableRow key={`${row.tipoBobina}-${idx}`}> pagination: {
<TableCell>{row.tipoBobina}</TableCell> paginationModel: { pageSize: 10, page: 0 },
<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> pageSizeOptions={[5, 10, 25, 50]}
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>{row.kilosUtilizadosMesA.toLocaleString('es-AR')}</TableCell> disableRowSelectionOnClick
<TableCell align="right">{row.kilosUtilizadosMesB.toLocaleString('es-AR')}</TableCell> // hideFooterSelectedRowCount // Ya se maneja en CustomFooter
<TableCell align="right">{row.diferenciaKilosUtilizados.toLocaleString('es-AR')}</TableCell> />
</TableRow> </Paper>
))}
</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>
)} )}
{!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> </Box>
); );
}; };

View File

@@ -1,26 +1,20 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody, TableFooter
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto'; import type { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto';
import SeleccionaReporteConsumoBobinasPublicacion from './SeleccionaReporteConsumoBobinasPublicacion'; import SeleccionaReporteConsumoBobinasPublicacion from './SeleccionaReporteConsumoBobinasPublicacion';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; 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 ReporteConsumoBobinasPublicacionPage: React.FC = () => {
const [reportData, setReportData] = useState<ConsumoBobinasPublicacionDto[]>([]); const [reportData, setReportData] = useState<ConsumoBobinasPublicacionDto[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -31,6 +25,7 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
const [currentParams, setCurrentParams] = useState<{ const [currentParams, setCurrentParams] = useState<{
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
nombrePublicacion?: string; // Mantenido para nombre de archivo, no afecta al título DataGrid
} | null>(null); } | null>(null);
const handleGenerarReporte = useCallback(async (params: { const handleGenerarReporte = useCallback(async (params: {
@@ -45,8 +40,10 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
try { try {
const data = await reportesService.getConsumoBobinasPorPublicacion(params); const data = await reportesService.getConsumoBobinasPorPublicacion(params);
setReportData(data); const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.nombrePlanta}-${item.nombrePublicacion}-${index}` }));
if (data.length === 0) { setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -69,28 +66,28 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
}, []); }, []);
const handleExportToExcel = useCallback(() => { const handleExportToExcel = useCallback(() => {
if (reportData.length === 0) { if (reportData.length === 0) {
alert("No hay datos para exportar."); alert("No hay datos para exportar.");
return; return;
} }
const dataToExport = reportData.map(item => ({ const dataToExport = reportData.map(({ ...rest }) => ({
"Planta": item.nombrePlanta, "Planta": rest.nombrePlanta,
"Publicación": item.nombrePublicacion, "Publicación": rest.nombrePublicacion,
"Total Kilos": item.totalKilos, "Total Kilos": rest.totalKilos,
"Cantidad Bobinas": item.cantidadBobinas, "Cantidad Bobinas": rest.cantidadBobinas,
})); }));
// Calcular totales generales
const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0); const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0);
const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0); const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0);
dataToExport.push({ dataToExport.push({
"Planta": "TOTAL GENERAL", "Planta": "TOTAL GENERAL",
"Publicación": "", "Publicación": "", // Celda vacía para alineación
"Total Kilos": totalGeneralKilos, "Total Kilos": totalGeneralKilos,
"Cantidad Bobinas": totalGeneralBobinas, "Cantidad Bobinas": totalGeneralBobinas,
}); });
const ws = XLSX.utils.json_to_sheet(dataToExport); const ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {}); const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => { ws['!cols'] = headers.map(h => {
@@ -117,7 +114,7 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
setLoadingPdf(true); setLoadingPdf(true);
setError(null); setError(null);
try { try {
const blob = await reportesService.getConsumoBobinasPorPublicacionPdf(currentParams); const blob = await reportesService.getConsumoBobinasPorPublicacionPdf(currentParams); // solo fechaDesde, fechaHasta
if (blob.type === "application/json") { if (blob.type === "application/json") {
const text = await blob.text(); const text = await blob.text();
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF."; const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
@@ -134,10 +131,100 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
} }
}, [currentParams]); }, [currentParams]);
const groupedData = reportData.length > 0 ? groupDataByPlanta(reportData) : {}; const columns: GridColDef[] = [
const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0); { field: 'nombrePlanta', headerName: 'Planta', width: 200, flex: 1 },
const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0); { 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) { if (showParamSelector) {
return ( return (
@@ -171,55 +258,33 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && ( {!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> <Paper sx={{
<Table stickyHeader size="small"> height: 'calc(100vh - 380px)', // Ajustar altura para dar espacio al footer
<TableHead> width: '100%',
<TableRow> mt: 2,
<TableCell sx={{ fontWeight: 'bold' }}>Planta</TableCell> '& .MuiDataGrid-footerContainer': { // Asegurar que el contenedor del footer tenga suficiente espacio
<TableCell sx={{ fontWeight: 'bold' }}>Publicación</TableCell> minHeight: '52px', // o el alto que necesite tu CustomFooter
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Total Kilos</TableCell> }
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Cant. Bobinas</TableCell> }}>
</TableRow> <DataGrid
</TableHead> rows={rows}
<TableBody> columns={columns}
{Object.entries(groupedData).map(([plantaKey, publicaciones]) => { localeText={esES.components.MuiDataGrid.defaultProps.localeText}
const totalKilosPlanta = publicaciones.reduce((sum, item) => sum + item.totalKilos, 0); slots={{ footer: CustomFooter }}
const totalBobinasPlanta = publicaciones.reduce((sum, item) => sum + item.cantidadBobinas, 0); density="compact"
return ( // pageSizeOptions={[10, 25, 50]} // Descomentar si deseas que el usuario cambie el tamaño de página
<React.Fragment key={plantaKey}> // initialState={{
<TableRow sx={{ backgroundColor: 'rgba(0, 0, 0, 0.08)' }}> // pagination: {
<TableCell colSpan={4} sx={{ fontWeight: 'bold' }}>{plantaKey}</TableCell> // paginationModel: { pageSize: 25, page: 0 },
</TableRow> // },
{publicaciones.map((pub, pubIdx) => ( // }}
<TableRow key={`${plantaKey}-${pubIdx}`}> // autoHeight // Si se usa autoHeight, el `height` del Paper no aplicará scroll a la tabla
<TableCell></TableCell> {/* Columna Planta vacía para esta fila */} />
<TableCell>{pub.nombrePublicacion}</TableCell> </Paper>
<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>
)} )}
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)} {!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box> </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 React, { useState, useCallback } from 'react';
import { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto'; import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto';
import type { BalanceCuentaDistDto } from '../../models/dtos/Reportes/BalanceCuentaDistDto'; import type { BalanceCuentaDistDto } from '../../models/dtos/Reportes/BalanceCuentaDistDto';
@@ -12,22 +13,214 @@ import SeleccionaReporteCuentasDistribuidores from './SeleccionaReporteCuentasDi
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; 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 ReporteCuentasDistribuidoresPage: React.FC = () => {
const [reportData, setReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null); const [originalReportData, setOriginalReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
const [loading, setLoading] = useState(false); const [movimientosConSaldo, setMovimientosConSaldo] = useState<MovimientoConSaldo[]>([]);
const [loadingPdf, setLoadingPdf] = useState(false); const [notasConSaldo, setNotasConSaldo] = useState<NotaConSaldo[]>([]);
const [error, setError] = useState<string | null>(null); 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 [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
const [showParamSelector, setShowParamSelector] = useState(true); const [showParamSelector, setShowParamSelector] = useState<boolean>(true);
const [currentParams, setCurrentParams] = useState<{ const [currentParams, setCurrentParams] = useState<{
idDistribuidor: number; idDistribuidor: number;
idEmpresa: number; idEmpresa: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
nombreDistribuidor?: string; // Para el PDF y nombre de archivo nombreDistribuidor?: string;
nombreEmpresa?: string; // Para el PDF y nombre de archivo nombreEmpresa?: string;
} | null>(null); } | 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: { const handleGenerarReporte = useCallback(async (params: {
idDistribuidor: number; idDistribuidor: number;
idEmpresa: number; idEmpresa: number;
@@ -35,16 +228,18 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
fechaHasta: string; fechaHasta: string;
}) => { }) => {
setLoading(true); setLoading(true);
setError(null);
setApiErrorParams(null); setApiErrorParams(null);
setReportData(null); setOriginalReportData(null);
setMovimientosConSaldo([]);
// Obtener nombres para el PDF/Excel setNotasConSaldo([]);
const distService = (await import('../../services/Distribucion/distribuidorService')).default; setPagosConSaldo([]);
const distData = await distService.getDistribuidorById(params.idDistribuidor);
const empService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empService.getEmpresaById(params.idEmpresa);
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({ setCurrentParams({
...params, ...params,
nombreDistribuidor: distData?.nombre, nombreDistribuidor: distData?.nombre,
@@ -53,17 +248,17 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
try { try {
const data = await reportesService.getReporteCuentasDistribuidor(params); const data = await reportesService.getReporteCuentasDistribuidor(params);
setReportData(data); setOriginalReportData(data);
const noData = (!data.entradasSalidas?.length && !data.debitosCreditos?.length && !data.pagos?.length && !data.saldos?.length); const { movs, notas, pagos } = calcularSaldosPorSeccion(data);
if (noData) { setMovimientosConSaldo(movs);
setError("No se encontraron datos para los parámetros seleccionados."); setNotasConSaldo(notas);
} setPagosConSaldo(pagos);
setShowParamSelector(false); setShowParamSelector(false);
} catch (err: any) { } catch (error: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message const msg = axios.isAxiosError(error) && error.response?.data?.message
? err.response.data.message ? error.response.data.message
: 'Ocurrió un error al generar el reporte.'; : 'Error al generar el reporte';
setApiErrorParams(message); setApiErrorParams(msg);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -71,166 +266,46 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
const handleVolverAParametros = useCallback(() => { const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true); setShowParamSelector(true);
setReportData(null); setOriginalReportData(null);
setError(null); setMovimientosConSaldo([]);
setApiErrorParams(null); setNotasConSaldo([]);
setCurrentParams(null); setPagosConSaldo([]);
}, []); }, []);
const handleExportToExcel = useCallback(() => { const handleExportToExcel = useCallback(() => {
if (!reportData) { if (!originalReportData) return;
alert("No hay datos para exportar.");
return;
}
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
if (movimientosConSaldo.length) {
if (reportData.saldos?.length) { const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
const saldosToExport = reportData.saldos.map(item => ({ "Saldo Actual": item.monto })); XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
const wsSaldos = XLSX.utils.json_to_sheet(saldosToExport);
XLSX.utils.book_append_sheet(wb, wsSaldos, "SaldoActual");
} }
if (reportData.entradasSalidas?.length) { if (notasConSaldo.length) {
const esToExport = reportData.entradasSalidas.map(item => ({ const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }), XLSX.utils.book_append_sheet(wb, ws, 'Notas');
"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 (reportData.debitosCreditos?.length) { if (pagosConSaldo.length) {
const dcToExport = reportData.debitosCreditos.map(item => ({ const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }), XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
"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 (reportData.pagos?.length) { XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
const paToExport = reportData.pagos.map(item => ({ }, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
"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]);
const handleGenerarYAbrirPdf = useCallback(async () => { const handleGenerarYAbrirPdf = useCallback(async () => {
if (!currentParams) { if (!currentParams) return;
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
return;
}
setLoadingPdf(true); setLoadingPdf(true);
setError(null);
try { try {
const blob = await reportesService.getReporteCuentasDistribuidorPdf(currentParams); const blob = await reportesService.getReporteCuentasDistribuidorPdf(currentParams);
if (blob.type === "application/json") { window.open(URL.createObjectURL(blob), '_blank');
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 { } catch {
setError('Ocurrió un error al generar el PDF.'); /* manejar error */
} finally { } finally {
setLoadingPdf(false); setLoadingPdf(false);
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( 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}> <Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteCuentasDistribuidores <SeleccionaReporteCuentasDistribuidores
onGenerarReporte={handleGenerarReporte} 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 ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5">Reporte: Cuenta Corriente Distribuidor</Typography> <Typography variant="h5">Cuenta Corriente Distribuidor</Typography>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small"> <Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={!originalReportData} size="small">
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"} {loadingPdf ? <CircularProgress size={20} /> : 'Abrir PDF'}
</Button> </Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small"> <Button onClick={handleExportToExcel} variant="outlined" disabled={!originalReportData} size="small">
Exportar a Excel Exportar a Excel
</Button> </Button>
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small"> <Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
@@ -260,33 +343,31 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
</Box> </Box>
</Box> </Box>
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>} <Paper sx={{ p: 2, mb: 2 }}>
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} <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" sx={{ mt: 2 }}>Movimientos de Entrada / Salida</Typography>
<> {renderDataGrid(movimientosConSaldo, cols.mov)}
<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" gutterBottom>Movimientos (Entradas/Salidas)</Typography> <Typography variant="h6" sx={{ mt: 2 }}>Notas de Crédito / Débito</Typography>
{reportData.entradasSalidas && reportData.entradasSalidas.length > 0 ? {renderDataGrid(notasConSaldo, cols.notas)}
renderEntradasSalidasTable(reportData.entradasSalidas) : <Typography sx={{ fontStyle: 'italic' }}>No hay movimientos de entradas/salidas.</Typography>}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Notas de Débito/Crédito</Typography> <Typography variant="h6" sx={{ mt: 2 }}>Pagos Recibidos / Realizados</Typography>
{reportData.debitosCreditos && reportData.debitosCreditos.length > 0 ? {renderDataGrid(pagosConSaldo, cols.pagos)}
renderDebitosCreditosTable(reportData.debitosCreditos) : <Typography sx={{ fontStyle: 'italic' }}>No hay notas de débito/crédito.</Typography>}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Pagos</Typography> <Paper sx={{ p: 2, mt: 3 }}>
{reportData.pagos && reportData.pagos.length > 0 ? <Typography variant="h6">Resumen Final</Typography>
renderPagosTable(reportData.pagos) : <Typography sx={{ fontStyle: 'italic' }}>No hay pagos registrados.</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>
{!loading && !error && (!reportData || (!reportData.entradasSalidas?.length && !reportData.debitosCreditos?.length && !reportData.pagos?.length && !reportData.saldos?.length)) && <Typography variant="subtitle1" sx={{ fontWeight: 'bold', mt: 1 }}>
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)} Saldo Final del Período: {(totalMov + totalNot + totalPag).toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
</Typography>
</Paper>
</Box> </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 { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme // Añadido SxProps, Theme
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto'; 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 SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; 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 ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | null>(null); const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -23,8 +83,34 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
idEmpresa: number; idEmpresa: number;
nombreEmpresa?: string; nombreEmpresa?: string;
} | null>(null); } | 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: { const handleGenerarReporte = useCallback(async (params: {
fecha: string; fecha: string;
@@ -33,29 +119,49 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setApiErrorParams(null); setApiErrorParams(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default; const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa); const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({...params, nombreEmpresa: empData?.nombre}); setCurrentParams({...params, nombreEmpresa: empData?.nombre});
setReportData(null); setReportData(null);
// Resetear totales
setTotalesCanillas(initialTotals);
setTotalesAccionistas(initialTotals);
setTotalesCanillasOtraFecha(initialTotals);
setTotalesAccionistasOtraFecha(initialTotals);
try { try {
const data = await reportesService.getReporteDistribucionCanillas(params); const data = await reportesService.getReporteDistribucionCanillas(params);
setReportData(data);
const noData = (!data.canillas || data.canillas.length === 0) && const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
(!data.canillasAccionistas || data.canillasAccionistas.length === 0) && (arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || item.remito || item.devueltos || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
(!data.canillasTodos || data.canillasTodos.length === 0) &&
(!data.controlDevolucionesDetalle || data.controlDevolucionesDetalle.length === 0); const processedData = {
if (noData) { 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."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
} catch (err: any) { } catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Ocurrió un error al generar el reporte.';
? err.response.data.message
: 'Ocurrió un error al generar el reporte.';
setApiErrorParams(message); setApiErrorParams(message);
setReportData(null);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -80,13 +186,18 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
if (data && data.length > 0) { if (data && data.length > 0) {
const exportedData = data.map(item => { const exportedData = data.map(item => {
const row: Record<string, any> = {}; const row: Record<string, any> = {};
// Excluir el 'id' generado para DataGrid si existe
const { id, ...itemData } = item;
Object.keys(fields).forEach(key => { Object.keys(fields).forEach(key => {
row[fields[key]] = item[key]; row[fields[key]] = (itemData as any)[key]; // Usar itemData
if (key === 'fecha' && item[key]) { if (key === 'fecha' && (itemData as any)[key]) {
row[fields[key]] = new Date(item[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' }); row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
} }
if ((key === 'totalRendir') && item[key] != null) { if ((key === 'totalRendir') && (itemData as any)[key] != null) {
row[fields[key]] = parseFloat(item[key]).toFixed(2); 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; 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" }); // Definición de campos para la exportación
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" }); const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" }); const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" }); const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", 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.canillas, "Canillitas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.controlDevolucionesRemitos, "CtrlDev_Remitos", { remito: "Remito Ingresado" }); formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.controlDevolucionesOtrosDias, "CtrlDev_OtrosDias", { devueltos: "Devueltos Otros Días" }); 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) { if (currentParams) {
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`; fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
fileName += `_${currentParams.fecha}`; fileName += `_${currentParams.fecha}`;
@@ -128,11 +241,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
} }
setLoadingPdf(true); setLoadingPdf(true);
setError(null); setError(null);
setPdfSoloTotales(soloTotales); // Guardar la opción para el nombre del archivo setPdfSoloTotales(soloTotales);
try { try {
const blob = await reportesService.getReporteDistribucionCanillasPdf({ const blob = await reportesService.getReporteDistribucionCanillasPdf({
...currentParams, ...currentParams,
soloTotales // Pasar el parámetro al servicio soloTotales
}); });
if (blob.type === "application/json") { if (blob.type === "application/json") {
const text = await blob.text(); const text = await blob.text();
@@ -150,53 +263,93 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
} }
}, [currentParams]); }, [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>; // --- Definiciones de Columnas ---
return ( const commonColumns: GridColDef[] = [
<> { field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2, fontWeight:'bold' }}>{title}</Typography> { field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}> { field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
<Table stickyHeader size="small"> { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
<TableHead> { 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)) },
<TableRow> { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
<TableCell>Publicación</TableCell> ];
<TableCell>{isAllDto ? 'Tipo Vendedor' : 'Canillita'}</TableCell>
{ (data[0] as DetalleDistribucionCanillaDto).fecha && <TableCell>Fecha Mov.</TableCell> } const commonColumnsWithFecha: GridColDef[] = [
<TableCell align="right">Llevados</TableCell> { field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
<TableCell align="right">Devueltos</TableCell> { field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 },
<TableCell align="right">Vendidos</TableCell> { field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
<TableCell align="right">A Rendir</TableCell> { field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
</TableRow> { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
</TableHead> { 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)) },
<TableBody> { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
{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; const columnsTodos: GridColDef[] = [
return ( { field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
<TableRow key={`${title.replace(/\s+/g, '')}-${idx}`}> { field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 },
<TableCell>{item.publicacion}</TableCell> { field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
<TableCell>{isAllDto ? item.tipoVendedor : item.canilla}</TableCell> { field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ fechaMov && <TableCell>{fechaMov}</TableCell> } { 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)) },
<TableCell align="right">{item.totalCantSalida.toLocaleString('es-AR')}</TableCell> { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
<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> // --- Custom Footers ---
</TableRow> const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno
)})} const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => {
</TableBody> if (!colConfig) return { width: 100, textAlign: 'right' as const, pr: isPlaceholder ? 0 : 1, fontWeight: 'bold' };
</Table> const defaultWidth = colConfig.field === 'publicacion' ? 200 : (colConfig.field === 'canilla' || colConfig.field === 'tipoVendedor' ? 150 : 100);
</TableContainer> 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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}> <Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteDetalleDistribucionCanillas <SeleccionaReporteDetalleDistribucionCanillas
onGenerarReporte={handleGenerarReporte} onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros} onCancel={handleVolverAParametros} // Aunque el componente no lo use directamente.
isLoading={loading} isLoading={loading}
apiErrorMessage={apiErrorParams} apiErrorMessage={apiErrorParams}
/> />
@@ -208,7 +361,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <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' }}> <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small"> <Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"} {loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
@@ -225,22 +378,27 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && ( {!loading && !error && reportData && (
<> <>
{renderTableData(reportData.canillas, "Canillitas")} <DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
{renderTableData(reportData.canillasAccionistas, "Accionistas")} <DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
{renderTableData(reportData.canillasTodos, "Resumen por Tipo de Vendedor", true)}
<DataGridSection title="Resumen por Tipo de Vendedor" data={reportData.canillasTodos || []} columns={columnsTodos} height={220}/>
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 && {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 && {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))) && {!loading && !error && reportData &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)} 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> </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 { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionCanillasImporteDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasImporteDto'; import type { ListadoDistribucionCanillasImporteDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasImporteDto';
import SeleccionaReporteListadoDistribucionCanillasImporte from './SeleccionaReporteListadoDistribucionCanillasImporte'; import SeleccionaReporteListadoDistribucionCanillasImporte from './SeleccionaReporteListadoDistribucionCanillasImporte';
@@ -37,11 +39,13 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
const pubService = (await import('../../services/Distribucion/publicacionService')).default; const pubService = (await import('../../services/Distribucion/publicacionService')).default;
const pubData = await pubService.getPublicacionById(params.idPublicacion); const pubData = await pubService.getPublicacionById(params.idPublicacion);
setCurrentParams({...params, nombrePublicacion: pubData?.nombre}); setCurrentParams({...params, nombrePublicacion: pubData?.nombre}); // Acceder a publicacion.nombre
try { try {
const data = await reportesService.getListadoDistribucionCanillasImporte(params); const data = await reportesService.getListadoDistribucionCanillasImporte(params);
setReportData(data); // 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)
if (data.length === 0) { 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."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -69,23 +73,31 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
alert("No hay datos para exportar."); alert("No hay datos para exportar.");
return; return;
} }
const dataToExport = reportData.map(item => ({ const dataToExport = reportData.map(({ ...rest }) => ({ // Excluir 'id'
"Fecha": item.fecha, // Ya viene como string dd/MM/yyyy del SP "Fecha": rest.fecha,
"Llevados": item.llevados, "Llevados": rest.llevados,
"Devueltos": item.devueltos, "Devueltos": rest.devueltos,
"Vendidos": item.vendidos, "Vendidos": rest.vendidos,
"Imp. Publicación": item.totalRendirPublicacion, "Importe Publicación": rest.totalRendirPublicacion,
"A Rendir": item.totalRendirGeneral, "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 ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0]); const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => { ws['!cols'] = headers.map(h => {
const maxLen = dataToExport.reduce((prev, row) => { const maxLen = Math.max(...dataToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
const cell = (row as any)[h]?.toString() ?? ''; return { wch: maxLen + 2 };
return Math.max(prev, cell.length);
}, h.length);
return { wch: maxLen + 2 };
}); });
ws['!freeze'] = { xSplit: 0, ySplit: 1 }; ws['!freeze'] = { xSplit: 0, ySplit: 1 };
@@ -126,6 +138,68 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -144,7 +218,7 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <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 }}> <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"} {loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
@@ -158,36 +232,32 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && ( {!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> <Paper sx={{
<Table stickyHeader size="small"> height: 'calc(100vh - 280px)', // Ajusta esta altura para el footer
<TableHead> width: '100%',
<TableRow> mt: 2,
<TableCell>Fecha</TableCell> '& .MuiDataGrid-footerContainer': {
<TableCell align="right">Llevados</TableCell> minHeight: '52px', // O el alto que necesite tu CustomFooter
<TableCell align="right">Devueltos</TableCell> }
<TableCell align="right">Vendidos</TableCell> }}>
<TableCell align="right">Importe Publicación</TableCell> <DataGrid
<TableCell align="right">A Rendir</TableCell> rows={rows}
</TableRow> columns={columns}
</TableHead> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableBody> slots={{ footer: CustomFooter }}
{reportData.map((row, idx) => ( density="compact"
<TableRow key={`importe-${idx}`}> // pageSizeOptions={[10, 25, 50]} // Descomentar si deseas paginación
<TableCell>{row.fecha}</TableCell> // initialState={{
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell> // pagination: {
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell> // paginationModel: { pageSize: 25, page: 0 },
<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> </Paper>
))}
</TableBody>
</Table>
</TableContainer>
)} )}
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)} {!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box> </Box>

View File

@@ -1,16 +1,36 @@
// src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto'; 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 SeleccionaReporteListadoDistribucionCanillas from './SeleccionaReporteListadoDistribucionCanillas';
import * as XLSX from 'xlsx'; 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 ReporteListadoDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionCanillasResponseDto | null>(null); const [reportData, setReportData] = useState<ListadoDistribucionCanillasResponseDto | null>(null);
const [detalleDiarioCalculado, setDetalleDiarioCalculado] = useState<DetalleDiarioCanillasExtendido[]>([]);
const [promediosPorDiaCalculado, setPromediosPorDiaCalculado] = useState<PromedioDiaCanillasExtendido[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false); const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -23,6 +43,22 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
nombrePublicacion?: string; nombrePublicacion?: string;
} | null>(null); } | 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: { const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
@@ -31,24 +67,99 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setApiErrorParams(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 pubService = (await import('../../services/Distribucion/publicacionService')).default;
const pubData = await pubService.getPublicacionById(params.idPublicacion); const pubData = await pubService.getPublicacionById(params.idPublicacion);
setCurrentParams({...params, nombrePublicacion: pubData?.nombre }); setCurrentParams({ ...params, nombrePublicacion: pubData?.nombre }); // Asumiendo que pubData es la tupla
try { try {
const data = await reportesService.getListadoDistribucionCanillas(params); 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."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
} catch (err: any) { } catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message // ... (manejo de errores)
? err.response.data.message
: 'Ocurrió un error al generar el reporte.';
setApiErrorParams(message);
setReportData(null);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -60,46 +171,61 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setError(null); setError(null);
setApiErrorParams(null); setApiErrorParams(null);
setCurrentParams(null); setCurrentParams(null);
setDetalleDiarioCalculado([]);
setPromediosPorDiaCalculado([]);
}, []); }, []);
const handleExportToExcel = useCallback(() => { const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.detalleSimple?.length && !reportData.promediosPorDia?.length)) { if (!detalleDiarioCalculado.length && !promediosPorDiaCalculado.length) {
alert("No hay datos para exportar."); alert("No hay datos para exportar.");
return; return;
} }
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
if (reportData.detalleSimple?.length) { if (detalleDiarioCalculado.length) {
const simpleToExport = reportData.detalleSimple.map(item => ({ const simpleToExport = detalleDiarioCalculado.map(({ id, ...rest }) => ({
"Día": item.dia, "Día": rest.dia,
"Llevados": item.llevados, "Llevados": rest.llevados,
"Devueltos": item.devueltos, "Devueltos": rest.devueltos,
"Vendidos": item.llevados - item.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); 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) { if (promediosPorDiaCalculado.length) {
const promediosToExport = reportData.promediosPorDia.map(item => ({ const promediosToExport = promediosPorDiaCalculado.map(({ id, ...rest }) => ({
"Día Semana": item.dia, "Día Semana": rest.dia,
"Cant. Días": item.cant, "Cant. Días": rest.cant,
"Prom. Llevados": item.promedio_Llevados, "Prom. Llevados": rest.promedio_Llevados,
"Prom. Devueltos": item.promedio_Devueltos, "Prom. Devueltos": rest.promedio_Devueltos,
"Prom. Vendidos": item.promedio_Ventas, "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); 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) { if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`; fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`; fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
} }
fileName += ".xlsx"; fileName += ".xlsx";
XLSX.writeFile(wb, fileName); XLSX.writeFile(wb, fileName);
}, [reportData, currentParams]); }, [detalleDiarioCalculado, promediosPorDiaCalculado, currentParams, totalesDetalle, totalesPromedios]);
const handleGenerarYAbrirPdf = useCallback(async () => { const handleGenerarYAbrirPdf = useCallback(async () => {
if (!currentParams) { if (!currentParams) {
@@ -110,7 +236,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setError(null); setError(null);
try { try {
const blob = await reportesService.getListadoDistribucionCanillasPdf(currentParams); const blob = await reportesService.getListadoDistribucionCanillasPdf(currentParams);
if (blob.type === "application/json") { if (blob.type === "application/json") {
const text = await blob.text(); const text = await blob.text();
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF."; const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
setError(msg); setError(msg);
@@ -126,6 +252,62 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -157,69 +339,54 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
</Button> </Button>
</Box> </Box>
</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>} {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && ( {!loading && !error && reportData && (
<> <>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Detalle Diario</Typography> <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Detalle Diario</Typography>
{reportData.detalleSimple && reportData.detalleSimple.length > 0 ? ( {detalleDiarioCalculado.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}> <Paper sx={{ height: 450, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<Table stickyHeader size="small"> <DataGrid
<TableHead> rows={detalleDiarioCalculado} // Usar los datos calculados
<TableRow> columns={columnsDetalle}
<TableCell>Día</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">Llevados</TableCell> density="compact"
<TableCell align="right">Devueltos</TableCell> slots={{ footer: CustomFooterDetalle }}
<TableCell align="right">Vendidos</TableCell> hideFooterSelectedRowCount
</TableRow> />
</TableHead> </Paper>
<TableBody> ) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de detalle diario.</Typography>)}
{reportData.detalleSimple.map((row, idx) => (
<TableRow key={`simple-${idx}`}>
<TableCell>{row.dia}</TableCell>
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{(row.llevados - row.devueltos).toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (<Typography>No hay datos de detalle diario.</Typography>)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography> <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
{reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? ( {promediosPorDiaCalculado.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '300px' }}> <Paper sx={{ height: 360, width: '100%', '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<Table stickyHeader size="small"> <DataGrid
<TableHead> rows={promediosPorDiaCalculado} // Usar los datos calculados
<TableRow> columns={columnsPromedios}
<TableCell>Día Semana</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">Cant. Días</TableCell> density="compact"
<TableCell align="right">Prom. Llevados</TableCell> slots={{ footer: CustomFooterPromedios }}
<TableCell align="right">Prom. Devueltos</TableCell> hideFooterSelectedRowCount
<TableCell align="right">Prom. Ventas</TableCell> sx={{
</TableRow> '& .MuiTablePagination-root': { // Oculta el paginador por defecto
</TableHead> display: 'none',
<TableBody> },
{reportData.promediosPorDia.map((row, idx) => ( }}
<TableRow key={`promedio-${idx}`}> />
<TableCell>{row.dia}</TableCell> </Paper>
<TableCell align="right">{row.cant.toLocaleString('es-AR')}</TableCell> ) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de promedios por día.</Typography>)}
<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>)}
</> </>
)} )}
{!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> </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 { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto'; import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral'; import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; import axios from 'axios';
const ReporteListadoDistribucionGeneralPage: React.FC = () => { interface TotalesComunes {
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null); 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 [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false); const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null); const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
const [showParamSelector, setShowParamSelector] = useState(true); const [showParamSelector, setShowParamSelector] = useState(true);
const [currentParams, setCurrentParams] = useState<{ const [currentParams, setCurrentParams] = useState<{
idPublicacion: number; fecha: string;
fechaDesde: string; // Primer día del mes idEmpresa: number;
fechaHasta: string; // Último día del mes nombreEmpresa?: string;
nombrePublicacion?: string; // Para el nombre del archivo
mesAnioParaNombreArchivo?: string; // Para el nombre del archivo (ej. YYYY-MM)
} | null>(null); } | 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: { const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number; fecha: string;
fechaDesde: string; idEmpresa: number;
fechaHasta: string;
}) => { }) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setApiErrorParams(null); setApiErrorParams(null);
// Para el nombre del archivo y título del PDF const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const pubService = (await import('../../services/Distribucion/publicacionService')).default; const empData = await empresaService.getEmpresaById(params.idEmpresa);
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, nombreEmpresa: empData?.nombre });
setReportData(null); // Limpiar datos antiguos
// Resetear totales
setTotalesCanillas(initialTotals);
setTotalesAccionistas(initialTotals);
setTotalesTodos(initialTotals);
setTotalesCanillasOtraFecha(initialTotals);
setTotalesAccionistasOtraFecha(initialTotals);
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
try { try {
const data = await reportesService.getListadoDistribucionGeneral(params); const data = await reportesService.getReporteDistribucionCanillas(params);
setReportData(data);
if ((!data.resumen || data.resumen.length === 0) && (!data.promediosPorDia || data.promediosPorDia.length === 0)) { 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."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -68,62 +134,96 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
}, []); }, []);
const handleExportToExcel = useCallback(() => { const handleExportToExcel = useCallback(() => {
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) { if (!reportData) {
alert("No hay datos para exportar."); alert("No hay datos para exportar.");
return; return;
} }
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
if (reportData.resumen?.length) { const formatAndSheet = (
const resumenToExport = reportData.resumen.map(item => ({ data: any[],
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-', sheetName: string,
"Tirada": item.cantidadTirada, fields: Record<string, string>,
"Sin Cargo": item.sinCargo, totals?: TotalesComunes
"Perdidos": item.perdidos, ) => {
"Llevados": item.llevados, if (data && data.length > 0) {
"Devueltos": item.devueltos, let exportedData = data.map(item => {
"Vendidos": item.vendidos, const row: Record<string, any> = {};
})); const { id, ...itemData } = item; // Excluir el 'id' generado
const wsResumen = XLSX.utils.json_to_sheet(resumenToExport); Object.keys(fields).forEach(key => {
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario"); 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) { if (totals) {
const promediosToExport = reportData.promediosPorDia.map(item => ({ const totalRow: Record<string, any> = {};
"Día Semana": item.dia, const fieldKeys = Object.keys(fields);
"Cant. Días": item.cantidadDias, totalRow[fields[fieldKeys[0]]] = "TOTALES"; // Título en la primera columna
"Prom. Tirada": item.promedioTirada, if (fields.totalCantSalida) totalRow[fields.totalCantSalida] = totals.totalCantSalida;
"Prom. Sin Cargo": item.promedioSinCargo, if (fields.totalCantEntrada) totalRow[fields.totalCantEntrada] = totals.totalCantEntrada;
"Prom. Perdidos": item.promedioPerdidos, if (fields.vendidos) totalRow[fields.vendidos] = totals.vendidos;
"Prom. Llevados": item.promedioLlevados, if (fields.totalRendir) totalRow[fields.totalRendir] = totals.totalRendir;
"Prom. Devueltos": item.promedioDevueltos, exportedData.push(totalRow);
"Prom. Vendidos": item.promedioVendidos, }
}));
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
}
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) { if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`; fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`; fileName += `_${currentParams.fecha}`;
} }
fileName += ".xlsx"; fileName += ".xlsx";
XLSX.writeFile(wb, fileName); 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) { if (!currentParams) {
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros."); setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
return; return;
} }
setLoadingPdf(true); setLoadingPdf(true);
setError(null); setError(null);
setPdfSoloTotales(soloTotales);
try { try {
const blob = await reportesService.getListadoDistribucionGeneralPdf({ const blob = await reportesService.getReporteDistribucionCanillasPdf({
idPublicacion: currentParams.idPublicacion, ...currentParams,
fechaDesde: currentParams.fechaDesde, // El servicio y SP esperan fechaDesde para el mes/año soloTotales
fechaHasta: currentParams.fechaHasta // El SP no usa esta, pero el servicio de reporte sí para el nombre
}); });
if (blob.type === "application/json") { if (blob.type === "application/json") {
const text = await blob.text(); const text = await blob.text();
@@ -141,13 +241,99 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}> <Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteListadoDistribucionGeneral <SeleccionaReporteDetalleDistribucionCanillas
onGenerarReporte={handleGenerarReporte} onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros} onCancel={handleVolverAParametros} // Asumo que no se usa, ya que el selector no tiene botón de cancelar
isLoading={loading} isLoading={loading}
apiErrorMessage={apiErrorParams} apiErrorMessage={apiErrorParams}
/> />
@@ -159,10 +345,13 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Listado Distribución General</Typography> <Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha}</Typography>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small"> <Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"} {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>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small"> <Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
Exportar a Excel Exportar a Excel
@@ -173,80 +362,151 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && ( {!loading && !error && reportData && (
<> <>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography> {/* Canillitas (del día) */}
{reportData.resumen && reportData.resumen.length > 0 ? ( <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (del día)</Typography>
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}> {rowsCanillas.length > 0 ? (
<Table stickyHeader size="small"> <Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<TableHead> <DataGrid
<TableRow> rows={rowsCanillas}
<TableCell>Fecha</TableCell> columns={commonColumns}
<TableCell align="right">Tirada</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">Sin Cargo</TableCell> density="compact"
<TableCell align="right">Perdidos</TableCell> slots={{ footer: CustomFooterCanillas }}
<TableCell align="right">Llevados</TableCell> hideFooterSelectedRowCount
<TableCell align="right">Devueltos</TableCell> />
<TableCell align="right">Vendidos</TableCell> </Paper>
</TableRow> ) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para canillitas (del día).</Typography>)}
</TableHead>
<TableBody> {/* Accionistas (del día) */}
{reportData.resumen.map((row, idx) => ( <Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (del día)</Typography>
<TableRow key={`resumen-${idx}`}> {rowsAccionistas.length > 0 ? (
<TableCell>{row.fecha ? new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell> <Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<TableCell align="right">{row.cantidadTirada.toLocaleString('es-AR')}</TableCell> <DataGrid
<TableCell align="right">{row.sinCargo.toLocaleString('es-AR')}</TableCell> rows={rowsAccionistas}
<TableCell align="right">{row.perdidos.toLocaleString('es-AR')}</TableCell> columns={commonColumns}
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell> density="compact"
<TableCell align="right">{row.vendidos.toLocaleString('es-AR')}</TableCell> slots={{ footer: CustomFooterAccionistas }}
</TableRow> hideFooterSelectedRowCount
))} />
</TableBody> </Paper>
</Table> ) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para accionistas (del día).</Typography>)}
</TableContainer>
) : (<Typography>No hay datos de resumen diario.</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> </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 { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
// Corregir importaciones de DTOs
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto'; 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 SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; 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 ReporteMovimientoBobinasEstadoPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null); const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -20,8 +33,16 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta: number; idPlanta: number;
nombrePlanta?: string;
} | null>(null); } | 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: { const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
@@ -33,8 +54,14 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
setCurrentParams(params); setCurrentParams(params);
try { try {
const data = await reportesService.getMovimientoBobinasEstado(params); 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."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -65,7 +92,6 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
// Hoja de Detalles
if (reportData.detalle?.length) { if (reportData.detalle?.length) {
const detalleToExport = reportData.detalle.map(item => ({ const detalleToExport = reportData.detalle.map(item => ({
"Tipo Bobina": item.tipoBobina, "Tipo Bobina": item.tipoBobina,
@@ -76,18 +102,11 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
})); }));
const wsDetalle = XLSX.utils.json_to_sheet(detalleToExport); const wsDetalle = XLSX.utils.json_to_sheet(detalleToExport);
const headersDetalle = Object.keys(detalleToExport[0] || {}); const headersDetalle = Object.keys(detalleToExport[0] || {});
wsDetalle['!cols'] = headersDetalle.map(h => { wsDetalle['!cols'] = headersDetalle.map(h => ({ wch: Math.max(...detalleToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
const maxLen = detalleToExport.reduce((prev, row) => {
const cell = (row as any)[h]?.toString() ?? '';
return Math.max(prev, cell.length);
}, h.length);
return { wch: maxLen + 2 };
});
wsDetalle['!freeze'] = { xSplit: 0, ySplit: 1 }; wsDetalle['!freeze'] = { xSplit: 0, ySplit: 1 };
XLSX.utils.book_append_sheet(wb, wsDetalle, "DetalleMovimientos"); XLSX.utils.book_append_sheet(wb, wsDetalle, "DetalleMovimientos");
} }
// Hoja de Totales
if (reportData.totales?.length) { if (reportData.totales?.length) {
const totalesToExport = reportData.totales.map(item => ({ const totalesToExport = reportData.totales.map(item => ({
"Tipo Movimiento": item.tipoMovimiento, "Tipo Movimiento": item.tipoMovimiento,
@@ -96,20 +115,15 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
})); }));
const wsTotales = XLSX.utils.json_to_sheet(totalesToExport); const wsTotales = XLSX.utils.json_to_sheet(totalesToExport);
const headersTotales = Object.keys(totalesToExport[0] || {}); const headersTotales = Object.keys(totalesToExport[0] || {});
wsTotales['!cols'] = headersTotales.map(h => { wsTotales['!cols'] = headersTotales.map(h => ({ wch: Math.max(...totalesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
const maxLen = totalesToExport.reduce((prev, row) => {
const cell = (row as any)[h]?.toString() ?? '';
return Math.max(prev, cell.length);
}, h.length);
return { wch: maxLen + 2 };
});
wsTotales['!freeze'] = { xSplit: 0, ySplit: 1 }; wsTotales['!freeze'] = { xSplit: 0, ySplit: 1 };
XLSX.utils.book_append_sheet(wb, wsTotales, "TotalesPorEstado"); XLSX.utils.book_append_sheet(wb, wsTotales, "TotalesPorEstado");
} }
let fileName = "ReporteMovimientoBobinasEstado"; let fileName = "ReporteMovimientoBobinasEstado";
if (currentParams) { 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"; fileName += ".xlsx";
XLSX.writeFile(wb, fileName); XLSX.writeFile(wb, fileName);
@@ -140,6 +154,56 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -155,10 +219,11 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
); );
} }
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <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 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button
onClick={handleGenerarYAbrirPdf} onClick={handleGenerarYAbrirPdf}
@@ -182,74 +247,51 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && ( {!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 }}> <Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
Detalle de Movimientos Detalle de Movimientos
</Typography> </Typography>
{reportData.detalle && reportData.detalle.length > 0 ? ( {rowsDetalle.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: '400px', mb: 3 }}> {/* Añadido mb: 3 para espaciado */} <Paper sx={{ width: '100%', mb: 3 }}>
<Table stickyHeader size="small"> <DataGrid
<TableHead> rows={rowsDetalle}
<TableRow> columns={columnsDetalle}
<TableCell>Tipo Bobina</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell>Nro Remito</TableCell> density="compact"
<TableCell>Fecha Movimiento</TableCell> sx={{ height: 'calc(100vh - 350px)' }}
<TableCell align="right">Cantidad</TableCell> slots={{ footer: CustomFooterDetalle }}
<TableCell>Tipo Movimiento</TableCell>
</TableRow> />
</TableHead> </Paper>
<TableBody>
{reportData.detalle.map((row, idx) => (
<TableRow key={`detalle-${idx}`}>
<TableCell>{row.tipoBobina}</TableCell>
<TableCell>{row.numeroRemito}</TableCell>
<TableCell>{row.fechaMovimiento ? new Date(row.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
<TableCell align="right">{row.cantidad.toLocaleString('es-AR')}</TableCell>
<TableCell>{row.tipoMovimiento}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : ( ) : (
<Typography sx={{ mb: 3 }}>No hay detalles de movimientos para mostrar.</Typography> <Typography sx={{ mb: 3, fontStyle: 'italic' }}>No hay detalles de movimientos para mostrar.</Typography>
)} )}
{/* Tabla de Totales por Estado */} <Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
<Typography variant="h6" gutterBottom>
Totales por Estado Totales por Estado
</Typography> </Typography>
{reportData.totales && reportData.totales.length > 0 ? ( {rowsTotales.length > 0 ? (
<TableContainer component={Paper} sx={{ maxWidth: '600px' }}> {/* Limitamos el ancho para tablas pequeñas */} <Paper sx={{ width: '100%', maxWidth: '700px' }}>
<Table size="small"> <DataGrid
<TableHead> rows={rowsTotales}
<TableRow> columns={columnsTotales}
<TableCell>Tipo Movimiento</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">Total Bobinas</TableCell> density="compact"
<TableCell align="right">Total Kilos</TableCell> autoHeight
</TableRow> hideFooter
</TableHead> disableRowSelectionOnClick
<TableBody> />
{reportData.totales.map((row, idx) => ( </Paper>
<TableRow key={`total-${idx}`}>
<TableCell>{row.tipoMovimiento}</TableCell>
<TableCell align="right">{row.totalBobinas.toLocaleString('es-AR')}</TableCell>
<TableCell align="right">{row.totalKilos.toLocaleString('es-AR')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : ( ) : (
<Typography>No hay totales para mostrar.</Typography> <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> </Box>
); );
}; };

View File

@@ -1,16 +1,22 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto'; import type { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto';
import SeleccionaReporteMovimientoBobinas from './SeleccionaReporteMovimientoBobinas'; import SeleccionaReporteMovimientoBobinas from './SeleccionaReporteMovimientoBobinas';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; 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 ReporteMovimientoBobinasPage: React.FC = () => {
const [reportData, setReportData] = useState<MovimientoBobinasDto[]>([]); const [reportData, setReportData] = useState<MovimientoBobinasDataGridDto[]>([]); // Usar el tipo extendido
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false); const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -20,8 +26,12 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta: number; idPlanta: number;
nombrePlanta?: string;
} | null>(null); } | null>(null);
const numberLocaleFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const handleGenerarReporte = useCallback(async (params: { const handleGenerarReporte = useCallback(async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
@@ -30,11 +40,20 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setApiErrorParams(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); setCurrentParams(params);
try { try {
const data = await reportesService.getMovimientoBobinas(params); const data = await reportesService.getMovimientoBobinas(params);
setReportData(data); // Añadir 'id' único a cada fila para DataGrid
if (data.length === 0) { 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."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -64,18 +83,35 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
} }
const dataToExport = reportData.map(item => ({ const dataToExport = reportData.map(item => ({
"Tipo Bobina": item.tipoBobina, "Tipo Bobina": item.tipoBobina,
"Bobinas Iniciales": item.bobinasIniciales, "Cant. Inicial": item.bobinasIniciales,
"Kg Iniciales": item.kilosIniciales, "Kg Iniciales": item.kilosIniciales,
"Bobinas Compradas": item.bobinasCompradas, "Compradas": item.bobinasCompradas,
"Kg Comprados": item.kilosComprados, "Kg Comprados": item.kilosComprados,
"Bobinas Consumidas": item.bobinasConsumidas, "Consumidas": item.bobinasConsumidas,
"Kg Consumidos": item.kilosConsumidos, "Kg Consumidos": item.kilosConsumidos,
"Bobinas Dañadas": item.bobinasDaniadas, "Dañadas": item.bobinasDaniadas,
"Kg Dañados": item.kilosDaniados, "Kg Dañados": item.kilosDaniados,
"Bobinas Finales": item.bobinasFinales, "Cant. Final": item.bobinasFinales,
"Kg Finales": item.kilosFinales, "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 ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0]); const headers = Object.keys(dataToExport[0]);
ws['!cols'] = headers.map(h => { ws['!cols'] = headers.map(h => {
@@ -91,7 +127,9 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
XLSX.utils.book_append_sheet(wb, ws, "MovimientoBobinas"); XLSX.utils.book_append_sheet(wb, ws, "MovimientoBobinas");
let fileName = "ReporteMovimientoBobinas"; let fileName = "ReporteMovimientoBobinas";
if (currentParams) { 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"; fileName += ".xlsx";
XLSX.writeFile(wb, fileName); XLSX.writeFile(wb, fileName);
@@ -122,6 +160,137 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -140,7 +309,7 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <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 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button
onClick={handleGenerarYAbrirPdf} onClick={handleGenerarYAbrirPdf}
@@ -164,47 +333,24 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
</Box> </Box>
</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>} {error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && ( {!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> <Paper sx={{ width: '100%', mt: 2 }}>
<Table stickyHeader size="small"> <DataGrid
<TableHead> rows={rows}
<TableRow> columns={columns}
<TableCell>Tipo Bobina</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">Cant. Ini.</TableCell> slots={{ footer: CustomFooter }}
<TableCell align="right">Kg Ini.</TableCell> density="compact"
<TableCell align="right">Compradas</TableCell> autoHeight // Para que se ajuste al contenido y al footer
<TableCell align="right">Kg Compr.</TableCell> hideFooterSelectedRowCount
<TableCell align="right">Consum.</TableCell> disableRowSelectionOnClick
<TableCell align="right">Kg Consum.</TableCell> />
<TableCell align="right">Dañadas</TableCell> </Paper>
<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 && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
</Box> </Box>
); );
}; };

View File

@@ -1,16 +1,22 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { import {
Box, Typography, Paper, CircularProgress, Alert, Button, Box, Typography, Paper, CircularProgress, Alert, Button
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material'; } 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 reportesService from '../../services/Reportes/reportesService';
import type { TiradasPublicacionesSeccionesDto } from '../../models/dtos/Reportes/TiradasPublicacionesSeccionesDto'; import type { TiradasPublicacionesSeccionesDto } from '../../models/dtos/Reportes/TiradasPublicacionesSeccionesDto';
import SeleccionaReporteTiradasPublicacionesSecciones from './SeleccionaReporteTiradasPublicacionesSecciones'; import SeleccionaReporteTiradasPublicacionesSecciones from './SeleccionaReporteTiradasPublicacionesSecciones';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import axios from 'axios'; import axios from 'axios';
// Interfaz extendida para DataGrid
interface TiradasPublicacionesSeccionesDataGridDto extends TiradasPublicacionesSeccionesDto {
id: string;
}
const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => { const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
const [reportData, setReportData] = useState<TiradasPublicacionesSeccionesDto[]>([]); const [reportData, setReportData] = useState<TiradasPublicacionesSeccionesDataGridDto[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false); const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -27,13 +33,16 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
mesAnioParaNombreArchivo?: string; mesAnioParaNombreArchivo?: string;
} | null>(null); } | 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: { const handleGenerarReporte = useCallback(async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta?: number | null; idPlanta?: number | null;
consolidado: boolean; consolidado: boolean;
}) => { }) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setApiErrorParams(null); setApiErrorParams(null);
@@ -47,15 +56,22 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
const plantaData = await plantaService.getPlantaById(params.idPlanta); const plantaData = await plantaService.getPlantaById(params.idPlanta);
plantaNombre = plantaData?.nombre ?? "N/A"; plantaNombre = plantaData?.nombre ?? "N/A";
} }
const mesAnioParts = params.fechaDesde.split('-'); // Formatear mes y año para el título/nombre de archivo
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`; // 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}); setCurrentParams({...params, nombrePublicacion: pubData?.nombre, nombrePlanta: plantaNombre, mesAnioParaNombreArchivo: mesAnioNombre});
try { try {
const data = await reportesService.getTiradasPublicacionesSecciones(params); const data = await reportesService.getTiradasPublicacionesSecciones(params);
setReportData(data); const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.nombreSeccion}-${index}` }));
if (data.length === 0) { setReportData(dataWithIds);
if (dataWithIds.length === 0) {
setError("No se encontraron datos para los parámetros seleccionados."); setError("No se encontraron datos para los parámetros seleccionados.");
} }
setShowParamSelector(false); setShowParamSelector(false);
@@ -84,13 +100,24 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
} }
const dataToExport = reportData.map(item => ({ const dataToExport = reportData.map(item => ({
"Nombre Sección": item.nombreSeccion, "Nombre Sección": item.nombreSeccion,
"Total Páginas Impresas": item.totalPaginasImpresas, "Páginas Impresas": item.totalPaginasImpresas, // Cambiado para coincidir con PDF
"Cantidad Ediciones": item.cantidadTiradas, "Total Ediciones": item.cantidadTiradas, // Cambiado para coincidir con PDF
"Total Páginas x Edición": item.totalPaginasEjemplares, "Pág. Por Edición (Promedio)": item.totalPaginasEjemplares, // Nombre según PDF
"Total Ejemplares": item.totalEjemplares, "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 ws = XLSX.utils.json_to_sheet(dataToExport);
const headers = Object.keys(dataToExport[0] || {}); const headers = Object.keys(dataToExport[0] || {});
ws['!cols'] = headers.map(h => { ws['!cols'] = headers.map(h => {
@@ -105,8 +132,8 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
let fileName = "ReporteTiradasSecciones"; let fileName = "ReporteTiradasSecciones";
if (currentParams) { if (currentParams) {
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`; fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
if (!currentParams.consolidado) fileName += `_Planta${currentParams.idPlanta}`; if (!currentParams.consolidado && currentParams.idPlanta) fileName += `_Planta${currentParams.idPlanta}`;
else fileName += "_Consolidado"; else if (currentParams.consolidado) fileName += "_Consolidado";
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`; fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
} }
fileName += ".xlsx"; fileName += ".xlsx";
@@ -138,6 +165,83 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
} }
}, [currentParams]); }, [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) { if (showParamSelector) {
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
@@ -156,7 +260,7 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <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 }}> <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"} {loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
@@ -169,39 +273,35 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
</Button> </Button>
</Box> </Box>
</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>} {loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} {error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData.length > 0 && ( {!loading && !error && reportData.length > 0 && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> <Paper sx={{ width: '100%', mt: 2 }}>
<Table stickyHeader size="small"> <DataGrid
<TableHead> rows={rows}
<TableRow> columns={columns}
<TableCell>Nombre Sección</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell align="right">Total Páginas Imp.</TableCell> slots={{ footer: CustomFooter }}
<TableCell align="right">Cant. Ediciones</TableCell> density="compact"
<TableCell align="right">Total Pág. x Edición</TableCell> sx={{ height: 'calc(100vh - 320px)' }} // Ajustar altura según sea necesario
<TableCell align="right">Total Ejemplares</TableCell> initialState={{
<TableCell align="right">Prom. Pág./Ejemplar</TableCell> pagination: {
</TableRow> paginationModel: { pageSize: 100, page: 0 },
</TableHead> },
<TableBody> }}
{reportData.map((row, idx) => ( pageSizeOptions={[25, 50 , 100]}
<TableRow key={`${row.nombreSeccion}-${idx}`}> disableRowSelectionOnClick
<TableCell>{row.nombreSeccion}</TableCell> />
<TableCell align="right">{row.totalPaginasImpresas.toLocaleString('es-AR')}</TableCell> </Paper>
<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>
)} )}
{!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> </Box>
); );
}; };

View File

@@ -240,13 +240,14 @@ const ReporteVentaMensualSecretariaPage: React.FC = () => {
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <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={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)'}}>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)'}}>Clarín</TableCell>
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Nación</TableCell> <TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Nación</TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell align="right">Día</TableCell>
<TableCell align="right">Tir. Coop.</TableCell><TableCell align="right">Dev. Coop.</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">Vta. Coop.</TableCell><TableCell align="right">Vta. Can.</TableCell>
<TableCell align="right">Tirada</TableCell><TableCell align="right">Devol.</TableCell><TableCell align="right">Venta</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 React, { useState, useEffect, useMemo } from 'react';
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, CircularProgress } from '@mui/material';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
const reportesSubModules = [ // Definición de los módulos de reporte con sus categorías, etiquetas y rutas
{ label: 'Existencia de Papel', path: 'existencia-papel' }, const allReportModules: { category: string; label: string; path: string }[] = [
{ label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' }, { category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' },
{ label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' }, { category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
{ label: 'Distribución General', path: 'listado-distribucion-general' }, { category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
{ label: 'Distribución Canillas', path: 'listado-distribucion-canillas' }, { category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' },
{ label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' }, { category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
{ label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' }, { category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' },
{ label: 'Det. Distribución Canillas', path: 'detalle-distribucion-canillas' }, { category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
{ label: 'Tiradas Pub./Sección', path: 'tiradas-publicaciones-secciones' }, { category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' },
{ label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' }, { category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
{ label: 'Consumo Bobinas/Pub.', path: 'consumo-bobinas-publicacion' }, { category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' },
{ label: 'Comparativa Cons. Bobinas', path: 'comparativa-consumo-bobinas' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
{ label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' }, { 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 ReportesIndexPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); 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(() => { useEffect(() => {
const currentBasePath = '/reportes'; const currentBasePath = '/reportes';
// Extrae la parte de la ruta que sigue a '/reportes/' const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
const subPathSegment = location.pathname.startsWith(currentBasePath + '/') const subPathSegment = pathParts[0];
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Toma solo el primer segmento
: undefined;
let activeTabIndex = -1; let activeReportFoundInEffect = false;
if (subPathSegment) { if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
activeTabIndex = reportesSubModules.findIndex( const activeReport = allReportModules.find(module => module.path === subPathSegment);
(subModule) => subModule.path === subPathSegment if (activeReport) {
); setExpandedCategory(activeReport.category);
} activeReportFoundInEffect = true;
if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
// Si estamos exactamente en '/reportes' y hay sub-módulos, navegar al primero.
if (location.pathname === currentBasePath && reportesSubModules.length > 0) {
navigate(reportesSubModules[0].path, { replace: true }); // Navega a la sub-ruta
// setSelectedSubTab(0); // Esto se manejará en la siguiente ejecución del useEffect debido al cambio de ruta
} else { } else {
setSelectedSubTab(false); // Ninguna sub-ruta activa o conocida, o no hay sub-módulos setExpandedCategory(false);
} }
} else {
setExpandedCategory(false);
} }
}, [location.pathname, navigate]); // Solo depende de location.pathname y navigate
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) {
// No es necesario setSelectedSubTab aquí directamente, el useEffect lo manejará. let firstReportToNavigate: { category: string; label: string; path: string } | null = null;
navigate(reportesSubModules[newValue].path); 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, 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 const handleReportClick = (reportPath: string) => {
if (reportesSubModules.length === 0) { 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 ( return (
<Box sx={{ p: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
<Typography variant="h5" gutterBottom>Módulo de Reportes</Typography> <CircularProgress />
<Typography>No hay reportes configurados.</Typography>
</Box> </Box>
); );
} }
return ( return (
<Box> // Contenedor principal que se adaptará a su padre
<Typography variant="h5" gutterBottom> // Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
Módulo de Reportes <Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
</Typography> {/* Panel Lateral para Navegación */}
<Paper square elevation={1}> <Paper
<Tabs elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
value={selectedSubTab} // 'false' es un valor válido para Tabs si ninguna pestaña está seleccionada square // Bordes rectos
onChange={handleSubTabChange} sx={{
indicatorColor="primary" width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
textColor="primary" minWidth: { xs: 200, sm: 220 },
variant="scrollable" height: '100%', // Ocupa toda la altura del Box padre
scrollButtons="auto" borderRight: (theme) => `1px solid ${theme.palette.divider}`,
aria-label="sub-módulos de reportes" 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) => ( <Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
<Tab key={subModule.path} label={subModule.label} /> Reportes
))} </Typography>
</Tabs> </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> </Paper>
<Box sx={{ pt: 2 }}>
{/* Outlet renderizará ReporteExistenciaPapelPage u otros {/* Área Principal para el Contenido del Reporte */}
Solo renderiza el Outlet si hay una pestaña seleccionada VÁLIDA. <Box
Si selectedSubTab es 'false' (porque ninguna ruta coincide con los sub-módulos), component="main"
se muestra el mensaje. sx={{
*/} flexGrow: 1, // Ocupa el espacio restante
{selectedSubTab !== false ? <Outlet /> : <Typography sx={{p:2}}>Seleccione un reporte del menú lateral o de las pestañas.</Typography>} 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>
</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 React, { useState, useEffect } from 'react';
import { import {
Box, Typography, TextField, Button, CircularProgress, Alert, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox FormControl, InputLabel, Select, MenuItem,
ToggleButtonGroup,
ToggleButton
} from '@mui/material'; } from '@mui/material';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import publicacionService from '../../services/Distribucion/publicacionService'; import publicacionService from '../../services/Distribucion/publicacionService';
@@ -116,17 +118,65 @@ const SeleccionaReporteListadoDistribucionCanillasImporte: React.FC<SeleccionaRe
disabled={isLoading} disabled={isLoading}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
/> />
<FormControlLabel <Box sx={{ mt: 2, mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
control={ <Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
<Checkbox Tipo de reporte
checked={esAccionista} </Typography>
onChange={(e) => setEsAccionista(e.target.checked)} <ToggleButtonGroup
disabled={isLoading} value={esAccionista ? 'accionistas' : 'canillitas'}
/> exclusive
} onChange={(_, value) => {
label="Ver Accionistas" if (value !== null) setEsAccionista(value === 'accionistas');
sx={{ mt: 1, mb: 1 }} }}
/> 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>} {apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</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 ReporteConsumoBobinasPublicacionPage from '../pages/Reportes/ReporteConsumoBobinasPublicacionPage';
import ReporteComparativaConsumoBobinasPage from '../pages/Reportes/ReporteComparativaConsumoBobinasPage'; import ReporteComparativaConsumoBobinasPage from '../pages/Reportes/ReporteComparativaConsumoBobinasPage';
import ReporteCuentasDistribuidoresPage from '../pages/Reportes/ReporteCuentasDistribuidoresPage'; import ReporteCuentasDistribuidoresPage from '../pages/Reportes/ReporteCuentasDistribuidoresPage';
import ReporteListadoDistribucionPage from '../pages/Reportes/ReporteListadoDistribucionPage';
import ReporteControlDevolucionesPage from '../pages/Reportes/ReporteControlDevolucionesPage';
// Auditorias // Auditorias
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage'; import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
@@ -177,6 +179,8 @@ const AppRoutes = () => {
<Route path="consumo-bobinas-publicacion" element={<ReporteConsumoBobinasPublicacionPage />} /> <Route path="consumo-bobinas-publicacion" element={<ReporteConsumoBobinasPublicacionPage />} />
<Route path="comparativa-consumo-bobinas" element={<ReporteComparativaConsumoBobinasPage />} /> <Route path="comparativa-consumo-bobinas" element={<ReporteComparativaConsumoBobinasPage />} />
<Route path="cuentas-distribuidores" element={<ReporteCuentasDistribuidoresPage />} /> <Route path="cuentas-distribuidores" element={<ReporteCuentasDistribuidoresPage />} />
<Route path="listado-distribucion-distribuidores" element={<ReporteListadoDistribucionPage />} />
<Route path="control-devoluciones" element={<ReporteControlDevolucionesPage />} />
</Route> </Route>
{/* Módulo de Radios (anidado) */} {/* 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 { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto';
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto'; import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto'; 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 { interface GetExistenciaPapelParams {
fechaDesde: string; // yyyy-MM-dd fechaDesde: string; // yyyy-MM-dd
fechaHasta: string; // yyyy-MM-dd fechaHasta: string; // yyyy-MM-dd
idPlanta?: number | null; idPlanta?: number | null;
consolidado: boolean; consolidado: boolean;
} }
const getExistenciaPapelPdf = async (params: GetExistenciaPapelParams): Promise<Blob> => { const getExistenciaPapelPdf = async (params: GetExistenciaPapelParams): Promise<Blob> => {
const queryParams: Record<string, string | number | boolean> = { const queryParams: Record<string, string | number | boolean> = {
fechaDesde: params.fechaDesde, fechaDesde: params.fechaDesde,
fechaHasta: params.fechaHasta, fechaHasta: params.fechaHasta,
consolidado: params.consolidado, consolidado: params.consolidado,
}; };
if (params.idPlanta && !params.consolidado) { if (params.idPlanta && !params.consolidado) {
queryParams.idPlanta = params.idPlanta; queryParams.idPlanta = params.idPlanta;
} }
const response = await apiClient.get('/reportes/existencia-papel/pdf', { const response = await apiClient.get('/reportes/existencia-papel/pdf', {
params: queryParams, params: queryParams,
responseType: 'blob', // ¡Importante para descargar archivos! responseType: 'blob', // ¡Importante para descargar archivos!
}); });
return response.data; // response.data será un Blob return response.data; // response.data será un Blob
}; };
const getExistenciaPapel = async (params: GetExistenciaPapelParams): Promise<ExistenciaPapelDto[]> => { const getExistenciaPapel = async (params: GetExistenciaPapelParams): Promise<ExistenciaPapelDto[]> => {
// Construir los query params, omitiendo idPlanta si es consolidado o no está definido // Construir los query params, omitiendo idPlanta si es consolidado o no está definido
const queryParams: Record<string, string | number | boolean> = { const queryParams: Record<string, string | number | boolean> = {
fechaDesde: params.fechaDesde, fechaDesde: params.fechaDesde,
fechaHasta: params.fechaHasta, fechaHasta: params.fechaHasta,
consolidado: params.consolidado, consolidado: params.consolidado,
}; };
if (params.idPlanta && !params.consolidado) { if (params.idPlanta && !params.consolidado) {
queryParams.idPlanta = params.idPlanta; queryParams.idPlanta = params.idPlanta;
} }
const response = await apiClient.get<ExistenciaPapelDto[]>('/reportes/existencia-papel', { params: queryParams }); const response = await apiClient.get<ExistenciaPapelDto[]>('/reportes/existencia-papel', { params: queryParams });
return response.data; return response.data;
}; };
const getMovimientoBobinas = async (params: { const getMovimientoBobinas = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta: number; idPlanta: number;
}): Promise<MovimientoBobinasDto[]> => { }): Promise<MovimientoBobinasDto[]> => {
const response = await apiClient.get<MovimientoBobinasDto[]>('/reportes/movimiento-bobinas', { params }); const response = await apiClient.get<MovimientoBobinasDto[]>('/reportes/movimiento-bobinas', { params });
return response.data; return response.data;
}; };
const getMovimientoBobinasPdf = async (params: { const getMovimientoBobinasPdf = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta: number; idPlanta: number;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/movimiento-bobinas/pdf', { const response = await apiClient.get('/reportes/movimiento-bobinas/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getMovimientoBobinasEstado = async (params: { const getMovimientoBobinasEstado = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta: number; idPlanta: number;
}): Promise<MovimientoBobinasPorEstadoResponseDto> => { // <- Devuelve el DTO combinado }): Promise<MovimientoBobinasPorEstadoResponseDto> => { // <- Devuelve el DTO combinado
const response = await apiClient.get<MovimientoBobinasPorEstadoResponseDto>('/reportes/movimiento-bobinas-estado', { params }); const response = await apiClient.get<MovimientoBobinasPorEstadoResponseDto>('/reportes/movimiento-bobinas-estado', { params });
return response.data; return response.data;
}; };
const getMovimientoBobinasEstadoPdf = async (params: { const getMovimientoBobinasEstadoPdf = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta: number; idPlanta: number;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/movimiento-bobinas-estado/pdf', { const response = await apiClient.get('/reportes/movimiento-bobinas-estado/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getListadoDistribucionGeneral = async (params: { const getListadoDistribucionGeneral = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; // YYYY-MM-DD (primer día del mes) fechaDesde: string; // YYYY-MM-DD (primer día del mes)
fechaHasta: string; // YYYY-MM-DD (último día del mes) fechaHasta: string; // YYYY-MM-DD (último día del mes)
}): Promise<ListadoDistribucionGeneralResponseDto> => { }): Promise<ListadoDistribucionGeneralResponseDto> => {
const response = await apiClient.get<ListadoDistribucionGeneralResponseDto>('/reportes/listado-distribucion-general', { params }); const response = await apiClient.get<ListadoDistribucionGeneralResponseDto>('/reportes/listado-distribucion-general', { params });
return response.data; return response.data;
}; };
const getListadoDistribucionGeneralPdf = async (params: { const getListadoDistribucionGeneralPdf = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-general/pdf', { const response = await apiClient.get('/reportes/listado-distribucion-general/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getListadoDistribucionCanillas = async (params: { const getListadoDistribucionCanillas = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<ListadoDistribucionCanillasResponseDto> => { }): Promise<ListadoDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ListadoDistribucionCanillasResponseDto>('/reportes/listado-distribucion-canillas', { params }); const response = await apiClient.get<ListadoDistribucionCanillasResponseDto>('/reportes/listado-distribucion-canillas', { params });
return response.data; return response.data;
}; };
const getListadoDistribucionCanillasPdf = async (params: { const getListadoDistribucionCanillasPdf = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-canillas/pdf', { const response = await apiClient.get('/reportes/listado-distribucion-canillas/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getListadoDistribucionCanillasImporte = async (params: { const getListadoDistribucionCanillasImporte = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
esAccionista: boolean; esAccionista: boolean;
}): Promise<ListadoDistribucionCanillasImporteDto[]> => { }): Promise<ListadoDistribucionCanillasImporteDto[]> => {
const response = await apiClient.get<ListadoDistribucionCanillasImporteDto[]>('/reportes/listado-distribucion-canillas-importe', { params }); const response = await apiClient.get<ListadoDistribucionCanillasImporteDto[]>('/reportes/listado-distribucion-canillas-importe', { params });
return response.data; return response.data;
}; };
const getListadoDistribucionCanillasImportePdf = async (params: { const getListadoDistribucionCanillasImportePdf = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
esAccionista: boolean; esAccionista: boolean;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/listado-distribucion-canillas-importe/pdf', { const response = await apiClient.get('/reportes/listado-distribucion-canillas-importe/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getVentaMensualSecretariaElDia = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElDiaDto[]> => { const getVentaMensualSecretariaElDia = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElDiaDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaElDiaDto[]>('/reportes/venta-mensual-secretaria/el-dia', { params }); const response = await apiClient.get<VentaMensualSecretariaElDiaDto[]>('/reportes/venta-mensual-secretaria/el-dia', { params });
return response.data; return response.data;
}; };
const getVentaMensualSecretariaElDiaPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => { 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' }); const response = await apiClient.get('/reportes/venta-mensual-secretaria/el-dia/pdf', { params, responseType: 'blob' });
return response.data; return response.data;
}; };
const getVentaMensualSecretariaElPlata = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElPlataDto[]> => { const getVentaMensualSecretariaElPlata = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaElPlataDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaElPlataDto[]>('/reportes/venta-mensual-secretaria/el-plata', { params }); const response = await apiClient.get<VentaMensualSecretariaElPlataDto[]>('/reportes/venta-mensual-secretaria/el-plata', { params });
return response.data; return response.data;
}; };
const getVentaMensualSecretariaElPlataPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => { 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' }); const response = await apiClient.get('/reportes/venta-mensual-secretaria/el-plata/pdf', { params, responseType: 'blob' });
return response.data; return response.data;
}; };
const getVentaMensualSecretariaTirDevo = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaTirDevoDto[]> => { const getVentaMensualSecretariaTirDevo = async (params: { fechaDesde: string; fechaHasta: string }): Promise<VentaMensualSecretariaTirDevoDto[]> => {
const response = await apiClient.get<VentaMensualSecretariaTirDevoDto[]>('/reportes/venta-mensual-secretaria/tirada-devolucion', { params }); const response = await apiClient.get<VentaMensualSecretariaTirDevoDto[]>('/reportes/venta-mensual-secretaria/tirada-devolucion', { params });
return response.data; return response.data;
}; };
const getVentaMensualSecretariaTirDevoPdf = async (params: { fechaDesde: string; fechaHasta: string }): Promise<Blob> => { 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' }); const response = await apiClient.get('/reportes/venta-mensual-secretaria/tirada-devolucion/pdf', { params, responseType: 'blob' });
return response.data; return response.data;
}; };
const getReporteDistribucionCanillas = async (params: { const getReporteDistribucionCanillas = async (params: {
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
}): Promise<ReporteDistribucionCanillasResponseDto> => { }): Promise<ReporteDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params }); const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params });
return response.data; return response.data;
}; };
const getReporteDistribucionCanillasPdf = async (params: { const getReporteDistribucionCanillasPdf = async (params: {
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
soloTotales: boolean; // Nuevo parámetro soloTotales: boolean; // Nuevo parámetro
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { // La ruta no necesita cambiar si el backend lo maneja 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í params, // soloTotales se enviará como query param si el backend lo espera así
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getTiradasPublicacionesSecciones = async (params: { const getTiradasPublicacionesSecciones = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta?: number | null; idPlanta?: number | null;
consolidado: boolean; consolidado: boolean;
}): Promise<TiradasPublicacionesSeccionesDto[]> => { }): Promise<TiradasPublicacionesSeccionesDto[]> => {
const response = await apiClient.get<TiradasPublicacionesSeccionesDto[]>('/reportes/tiradas-publicaciones-secciones', { params }); const response = await apiClient.get<TiradasPublicacionesSeccionesDto[]>('/reportes/tiradas-publicaciones-secciones', { params });
return response.data; return response.data;
}; };
const getTiradasPublicacionesSeccionesPdf = async (params: { const getTiradasPublicacionesSeccionesPdf = async (params: {
idPublicacion: number; idPublicacion: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta?: number | null; idPlanta?: number | null;
consolidado: boolean; consolidado: boolean;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/tiradas-publicaciones-secciones/pdf', { const response = await apiClient.get('/reportes/tiradas-publicaciones-secciones/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getConsumoBobinasSeccion = async (params: { const getConsumoBobinasSeccion = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta?: number | null; idPlanta?: number | null;
consolidado: boolean; consolidado: boolean;
}): Promise<ConsumoBobinasSeccionDto[]> => { }): Promise<ConsumoBobinasSeccionDto[]> => {
const response = await apiClient.get<ConsumoBobinasSeccionDto[]>('/reportes/consumo-bobinas-seccion', { params }); const response = await apiClient.get<ConsumoBobinasSeccionDto[]>('/reportes/consumo-bobinas-seccion', { params });
return response.data; return response.data;
}; };
const getConsumoBobinasSeccionPdf = async (params: { const getConsumoBobinasSeccionPdf = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
idPlanta?: number | null; idPlanta?: number | null;
consolidado: boolean; consolidado: boolean;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/consumo-bobinas-seccion/pdf', { const response = await apiClient.get('/reportes/consumo-bobinas-seccion/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getConsumoBobinasPorPublicacion = async (params: { const getConsumoBobinasPorPublicacion = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<ConsumoBobinasPublicacionDto[]> => { }): Promise<ConsumoBobinasPublicacionDto[]> => {
const response = await apiClient.get<ConsumoBobinasPublicacionDto[]>('/reportes/consumo-bobinas-publicacion', { params }); const response = await apiClient.get<ConsumoBobinasPublicacionDto[]>('/reportes/consumo-bobinas-publicacion', { params });
return response.data; return response.data;
}; };
const getConsumoBobinasPorPublicacionPdf = async (params: { const getConsumoBobinasPorPublicacionPdf = async (params: {
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/consumo-bobinas-publicacion/pdf', { const response = await apiClient.get('/reportes/consumo-bobinas-publicacion/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getComparativaConsumoBobinas = async (params: { const getComparativaConsumoBobinas = async (params: {
fechaInicioMesA: string; fechaFinMesA: string; fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string; fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean; idPlanta?: number | null; consolidado: boolean;
}): Promise<ComparativaConsumoBobinasDto[]> => { }): Promise<ComparativaConsumoBobinasDto[]> => {
const response = await apiClient.get<ComparativaConsumoBobinasDto[]>('/reportes/comparativa-consumo-bobinas', { params }); const response = await apiClient.get<ComparativaConsumoBobinasDto[]>('/reportes/comparativa-consumo-bobinas', { params });
return response.data; return response.data;
}; };
const getComparativaConsumoBobinasPdf = async (params: { const getComparativaConsumoBobinasPdf = async (params: {
fechaInicioMesA: string; fechaFinMesA: string; fechaInicioMesA: string; fechaFinMesA: string;
fechaInicioMesB: string; fechaFinMesB: string; fechaInicioMesB: string; fechaFinMesB: string;
idPlanta?: number | null; consolidado: boolean; idPlanta?: number | null; consolidado: boolean;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/comparativa-consumo-bobinas/pdf', { const response = await apiClient.get('/reportes/comparativa-consumo-bobinas/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; return response.data;
}; };
const getReporteCuentasDistribuidor = async (params: { const getReporteCuentasDistribuidor = async (params: {
idDistribuidor: number; idDistribuidor: number;
idEmpresa: number; idEmpresa: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<ReporteCuentasDistribuidorResponseDto> => { }): Promise<ReporteCuentasDistribuidorResponseDto> => {
const response = await apiClient.get<ReporteCuentasDistribuidorResponseDto>('/reportes/cuentas-distribuidores', { params }); const response = await apiClient.get<ReporteCuentasDistribuidorResponseDto>('/reportes/cuentas-distribuidores', { params });
return response.data; return response.data;
}; };
const getReporteCuentasDistribuidorPdf = async (params: { const getReporteCuentasDistribuidorPdf = async (params: {
idDistribuidor: number; idDistribuidor: number;
idEmpresa: number; idEmpresa: number;
fechaDesde: string; fechaDesde: string;
fechaHasta: string; fechaHasta: string;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/cuentas-distribuidores/pdf', { const response = await apiClient.get('/reportes/cuentas-distribuidores/pdf', {
params, params,
responseType: 'blob', responseType: 'blob',
}); });
return response.data; 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 = { const reportesService = {
getExistenciaPapel, getExistenciaPapel,
getExistenciaPapelPdf, getExistenciaPapelPdf,
getMovimientoBobinas, getMovimientoBobinas,
getMovimientoBobinasPdf, getMovimientoBobinasPdf,
getMovimientoBobinasEstado, getMovimientoBobinasEstado,
getMovimientoBobinasEstadoPdf, getMovimientoBobinasEstadoPdf,
getListadoDistribucionGeneral, getListadoDistribucionGeneral,
getListadoDistribucionGeneralPdf, getListadoDistribucionGeneralPdf,
getListadoDistribucionCanillas, getListadoDistribucionCanillas,
getListadoDistribucionCanillasPdf, getListadoDistribucionCanillasPdf,
getListadoDistribucionCanillasImporte, getListadoDistribucionCanillasImporte,
getListadoDistribucionCanillasImportePdf, getListadoDistribucionCanillasImportePdf,
getVentaMensualSecretariaElDia, getVentaMensualSecretariaElDia,
getVentaMensualSecretariaElDiaPdf, getVentaMensualSecretariaElDiaPdf,
getVentaMensualSecretariaElPlata, getVentaMensualSecretariaElPlata,
getVentaMensualSecretariaElPlataPdf, getVentaMensualSecretariaElPlataPdf,
getVentaMensualSecretariaTirDevo, getVentaMensualSecretariaTirDevo,
getVentaMensualSecretariaTirDevoPdf, getVentaMensualSecretariaTirDevoPdf,
getReporteDistribucionCanillas, getReporteDistribucionCanillas,
getReporteDistribucionCanillasPdf, getReporteDistribucionCanillasPdf,
getTiradasPublicacionesSecciones, getTiradasPublicacionesSecciones,
getTiradasPublicacionesSeccionesPdf, getTiradasPublicacionesSeccionesPdf,
getConsumoBobinasSeccion, getConsumoBobinasSeccion,
getConsumoBobinasSeccionPdf, getConsumoBobinasSeccionPdf,
getConsumoBobinasPorPublicacion, getConsumoBobinasPorPublicacion,
getConsumoBobinasPorPublicacionPdf, getConsumoBobinasPorPublicacionPdf,
getComparativaConsumoBobinas, getComparativaConsumoBobinas,
getComparativaConsumoBobinasPdf, getComparativaConsumoBobinasPdf,
getReporteCuentasDistribuidor, getReporteCuentasDistribuidor,
getReporteCuentasDistribuidorPdf, getReporteCuentasDistribuidorPdf,
getListadoDistribucionDistribuidores,
getListadoDistribucionDistribuidoresPdf,
getControlDevolucionesData,
getControlDevolucionesPdf,
}; };
export default reportesService; export default reportesService;