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:
@@ -127,14 +127,18 @@ namespace GestionIntegral.Api.Controllers
|
||||
{
|
||||
var planta = await _plantaRepository.GetByIdAsync(idPlanta.Value);
|
||||
nombrePlantaParam = planta?.Nombre ?? "N/A";
|
||||
} else if (consolidado) {
|
||||
}
|
||||
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
|
||||
{ // No consolidado pero idPlanta es NULL (aunque el servicio ya valida esto)
|
||||
nombrePlantaParam = "N/A";
|
||||
}
|
||||
// Solo añadir NomPlanta si NO es consolidado, porque el RDLC consolidado no lo tiene.
|
||||
if (!consolidado) {
|
||||
if (!consolidado)
|
||||
{
|
||||
parameters.Add(new ReportParameter("NomPlanta", nombrePlantaParam));
|
||||
}
|
||||
|
||||
@@ -1292,5 +1296,93 @@ namespace GestionIntegral.Api.Controllers
|
||||
}
|
||||
catch (Exception ex) { _logger.LogError(ex, "Error PDF Comparativa Consumo Bobinas."); return StatusCode(500, "Error interno."); }
|
||||
}
|
||||
|
||||
// GET: api/reportes/listado-distribucion-distribuidores
|
||||
[HttpGet("listado-distribucion-distribuidores")]
|
||||
[ProducesResponseType(typeof(ListadoDistribucionDistribuidoresResponseDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetListadoDistribucionDistribuidores(
|
||||
[FromQuery] int idDistribuidor,
|
||||
[FromQuery] int idPublicacion,
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // RR002
|
||||
|
||||
(IEnumerable<ListadoDistribucionDistSimpleDto> simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> promedios, string? error) result =
|
||||
await _reportesService.ObtenerListadoDistribucionDistribuidoresAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
|
||||
|
||||
if (result.error != null) return BadRequest(new { message = result.error });
|
||||
if ((result.simple == null || !result.simple.Any()) && (result.promedios == null || !result.promedios.Any()))
|
||||
{
|
||||
return NotFound(new { message = "No hay datos para el listado de distribución de distribuidores." });
|
||||
}
|
||||
|
||||
var response = new ListadoDistribucionDistribuidoresResponseDto
|
||||
{
|
||||
DetalleSimple = result.simple ?? Enumerable.Empty<ListadoDistribucionDistSimpleDto>(),
|
||||
PromediosPorDia = result.promedios ?? Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>()
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("listado-distribucion-distribuidores/pdf")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetListadoDistribucionDistribuidoresPdf(
|
||||
[FromQuery] int idDistribuidor,
|
||||
[FromQuery] int idPublicacion,
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
||||
|
||||
var (simple, promedios, error) = await _reportesService.ObtenerListadoDistribucionDistribuidoresAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if ((simple == null || !simple.Any()) && (promedios == null || !promedios.Any()))
|
||||
{
|
||||
return NotFound(new { message = "No hay datos para generar el PDF." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LocalReport report = new LocalReport();
|
||||
// USAREMOS EL MISMO RDLC QUE PARA CANILLITAS, YA QUE TIENE LA MISMA ESTRUCTURA DE DATASOURCES
|
||||
using (var fs = new FileStream("Controllers/Reportes/RDLC/ReporteListadoDistribucion.rdlc", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
report.LoadReportDefinition(fs);
|
||||
}
|
||||
// Nombres de DataSources deben coincidir con los del RDLC
|
||||
report.DataSources.Add(new ReportDataSource("DSListadoDistribucion", simple ?? new List<ListadoDistribucionDistSimpleDto>()));
|
||||
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionAgDias", promedios ?? new List<ListadoDistribucionDistPromedioDiaDto>()));
|
||||
|
||||
var publicacionData = await _publicacionRepository.GetByIdAsync(idPublicacion);
|
||||
var distribuidorData = await _distribuidorRepository.GetByIdAsync(idDistribuidor);
|
||||
|
||||
var parameters = new List<ReportParameter>
|
||||
{
|
||||
new ReportParameter("NomPubli", publicacionData.Publicacion?.Nombre ?? "N/A"),
|
||||
new ReportParameter("NomDist", distribuidorData.Distribuidor?.Nombre ?? "N/A"), // Parámetro para el RDLC
|
||||
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
|
||||
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
|
||||
};
|
||||
report.SetParameters(parameters);
|
||||
|
||||
byte[] pdfBytes = report.Render("PDF");
|
||||
string fileName = $"ListadoDistribucion_Dist{idDistribuidor}_Pub{idPublicacion}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf";
|
||||
return File(pdfBytes, "application/pdf", fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al generar PDF para Listado Distribucion (Distribuidores).");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del reporte.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
Task<IEnumerable<MovimientoBobinasDto>> GetMovimientoBobinasAsync(DateTime fechaInicio, int diasPeriodo, int idPlanta);
|
||||
Task<IEnumerable<MovimientoBobinaEstadoDetalleDto>> GetMovimientoBobinasEstadoDetalleAsync(DateTime fechaInicio, DateTime fechaFin, int idPlanta);
|
||||
Task<IEnumerable<MovimientoBobinaEstadoTotalDto>> GetMovimientoBobinasEstadoTotalesAsync(DateTime fechaInicio, DateTime fechaFin, int idPlanta);
|
||||
|
||||
// --- MÉTODOS AÑADIDOS AQUÍ ---
|
||||
Task<IEnumerable<ListadoDistribucionGeneralResumenDto>> GetListadoDistribucionGeneralResumenAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<ListadoDistribucionGeneralPromedioDiaDto>> GetListadoDistribucionGeneralPromedioDiaAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<ListadoDistribucionCanillasSimpleDto>> GetListadoDistribucionCanillasSimpleAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
@@ -40,5 +38,8 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
Task<IEnumerable<BalanceCuentaDebCredDto>> GetBalanceCuentDistDebCredEmpresaAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<BalanceCuentaPagosDto>> GetBalanceCuentDistPagosEmpresaAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<SaldoDto>> GetBalanceCuentSaldosEmpresasAsync(string destino, int idDestino, int idEmpresa);
|
||||
Task<IEnumerable<ListadoDistribucionDistSimpleDto>> GetListadoDistribucionDistSimpleAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<ListadoDistribucionDistPromedioDiaDto>> GetListadoDistribucionDistPromedioDiaAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,6 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
}
|
||||
}
|
||||
|
||||
// Implementaciones que faltaban
|
||||
public async Task<IEnumerable<ListadoDistribucionGeneralResumenDto>> GetListadoDistribucionGeneralResumenAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
const string spName = "dbo.SP_DistObtenerResumenMensual";
|
||||
@@ -419,5 +418,66 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
try { using var connection = _dbConnectionFactory.CreateConnection(); return await connection.QueryAsync<SaldoDto>(spName, parameters, commandType: CommandType.StoredProcedure); }
|
||||
catch (Exception ex) { _logger.LogError(ex, "Error SP {SPName}", spName); return Enumerable.Empty<SaldoDto>(); }
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ListadoDistribucionDistSimpleDto>> GetListadoDistribucionDistSimpleAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
const string spName = "dbo.SP_CantidadEntradaSalida";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@idDistribuidor", idDistribuidor, DbType.Int32);
|
||||
parameters.Add("@idPublicacion", idPublicacion, DbType.Int32);
|
||||
parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime);
|
||||
parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection(); // <--- CORREGIDO AQUÍ
|
||||
return await connection.QueryAsync<ListadoDistribucionDistSimpleDto>(spName, parameters, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al ejecutar SP {SPName} para Listado Distribucion Distribuidores (Simple). Params: Dist={idDistribuidor}, Pub={idPublicacion}, Desde={fechaDesde}, Hasta={fechaHasta}", spName, idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
|
||||
return Enumerable.Empty<ListadoDistribucionDistSimpleDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ListadoDistribucionDistPromedioDiaDto>> GetListadoDistribucionDistPromedioDiaAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
const string spName = "dbo.SP_CantidadEntradaSalidaCPromAgDia";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@idDistribuidor", idDistribuidor, DbType.Int32);
|
||||
parameters.Add("@idPublicacion", idPublicacion, DbType.Int32);
|
||||
parameters.Add("@fechaDesde", fechaDesde, DbType.DateTime);
|
||||
parameters.Add("@fechaHasta", fechaHasta, DbType.DateTime);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection(); // <--- CORREGIDO AQUÍ
|
||||
return await connection.QueryAsync<ListadoDistribucionDistPromedioDiaDto>(spName, parameters, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al ejecutar SP {SPName} para Listado Distribucion Distribuidores (Promedios). Params: Dist={idDistribuidor}, Pub={idPublicacion}, Desde={fechaDesde}, Hasta={fechaHasta}", spName, idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
|
||||
return Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
if (fechaDesde > fechaHasta)
|
||||
return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "La fecha 'Desde' no puede ser mayor que la fecha 'Hasta'.");
|
||||
|
||||
try
|
||||
{
|
||||
var simpleDataTask = this.GetListadoDistribucionDistSimpleAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
|
||||
var promediosDataTask = this.GetListadoDistribucionDistPromedioDiaAsync(idDistribuidor, idPublicacion, fechaDesde, fechaHasta);
|
||||
|
||||
await Task.WhenAll(simpleDataTask, promediosDataTask);
|
||||
|
||||
return (await simpleDataTask, await promediosDataTask, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error en ReportesService al obtener Listado Distribucion (Distribuidores).");
|
||||
return (Enumerable.Empty<ListadoDistribucionDistSimpleDto>(), Enumerable.Empty<ListadoDistribucionDistPromedioDiaDto>(), "Error interno al generar el reporte.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -61,5 +61,7 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
IEnumerable<SaldoDto> Saldos,
|
||||
string? Error
|
||||
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
|
||||
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+70fc84772161b499c8283a31b7a61246a6bcc46f")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1182a4cdee4fcdb55dc3f2dbfeeb2ec2187f2bea")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -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":{}}
|
||||
@@ -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":{}}
|
||||
@@ -5,13 +5,12 @@ import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordMod
|
||||
import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode; // Esto será el <Outlet /> que renderiza las páginas del módulo
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// Definir los módulos y sus rutas base
|
||||
const modules = [
|
||||
{ label: 'Inicio', path: '/' },
|
||||
{ label: 'Distribución', path: '/distribucion' }, // Asumiremos rutas base como /distribucion, /contables, etc.
|
||||
{ label: 'Distribución', path: '/distribucion' },
|
||||
{ label: 'Contables', path: '/contables' },
|
||||
{ label: 'Impresión', path: '/impresion' },
|
||||
{ label: 'Reportes', path: '/reportes' },
|
||||
@@ -19,24 +18,24 @@ const modules = [
|
||||
{ label: 'Usuarios', path: '/usuarios' },
|
||||
];
|
||||
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
const {
|
||||
user,
|
||||
logout,
|
||||
showForcedPasswordChangeModal,
|
||||
// ... (resto de las props de useAuth) ...
|
||||
isAuthenticated,
|
||||
isPasswordChangeForced,
|
||||
passwordChangeCompleted,
|
||||
showForcedPasswordChangeModal,
|
||||
setShowForcedPasswordChangeModal,
|
||||
isAuthenticated
|
||||
passwordChangeCompleted
|
||||
} = useAuth();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation(); // Para obtener la ruta actual
|
||||
|
||||
// Estado para el tab seleccionado
|
||||
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
||||
|
||||
// Efecto para sincronizar el tab seleccionado con la ruta actual
|
||||
useEffect(() => {
|
||||
const currentModulePath = modules.findIndex(module =>
|
||||
location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/'))
|
||||
@@ -44,14 +43,13 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
if (currentModulePath !== -1) {
|
||||
setSelectedTab(currentModulePath);
|
||||
} else if (location.pathname === '/') {
|
||||
setSelectedTab(0); // Seleccionar "Inicio" si es la raíz
|
||||
setSelectedTab(0);
|
||||
} else {
|
||||
setSelectedTab(false); // Ningún tab coincide (podría ser una sub-ruta no principal)
|
||||
setSelectedTab(false);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
|
||||
// ... (lógica de handleModalClose existente) ...
|
||||
if (passwordChangedSuccessfully) {
|
||||
passwordChangeCompleted();
|
||||
} else {
|
||||
@@ -65,12 +63,14 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedTab(newValue);
|
||||
navigate(modules[newValue].path); // Navegar a la ruta base del módulo
|
||||
navigate(modules[newValue].path);
|
||||
};
|
||||
|
||||
// Si el modal de cambio de clave forzado está activo, no mostramos la navegación principal aún.
|
||||
// El modal se superpone.
|
||||
// Determinar si el módulo actual es el de Reportes
|
||||
const isReportesModule = location.pathname.startsWith('/reportes');
|
||||
|
||||
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
|
||||
// ... (lógica del modal forzado sin cambios) ...
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
||||
<ChangePasswordModal
|
||||
@@ -78,15 +78,14 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
onClose={handleModalClose}
|
||||
isFirstLogin={isPasswordChangeForced}
|
||||
/>
|
||||
{/* Podrías querer un fondo o layout mínimo aquí si el modal no es pantalla completa */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<AppBar position="static">
|
||||
{/* ... (Toolbar y Tabs sin cambios) ... */}
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Sistema de Gestión - El Día
|
||||
@@ -95,24 +94,23 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
{isAuthenticated && !isPasswordChangeForced && (
|
||||
<Button
|
||||
color="inherit"
|
||||
onClick={() => setShowForcedPasswordChangeModal(true)} // Ahora abre el modal
|
||||
onClick={() => setShowForcedPasswordChangeModal(true)}
|
||||
>
|
||||
Cambiar Contraseña
|
||||
</Button>
|
||||
)}
|
||||
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
|
||||
</Toolbar>
|
||||
{/* Navegación Principal por Módulos */}
|
||||
<Paper square elevation={0} > {/* Usamos Paper para un fondo consistente para los Tabs */}
|
||||
<Paper square elevation={0} >
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onChange={handleTabChange}
|
||||
indicatorColor="secondary" // O "primary"
|
||||
textColor="inherit" // O "primary" / "secondary"
|
||||
variant="scrollable" // Permite scroll si hay muchos tabs
|
||||
scrollButtons="auto" // Muestra botones de scroll si es necesario
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="módulos principales"
|
||||
sx={{ backgroundColor: 'primary.main', color: 'white' }} // Color de fondo para los tabs
|
||||
sx={{ backgroundColor: 'primary.main', color: 'white' }}
|
||||
>
|
||||
{modules.map((module) => (
|
||||
<Tab key={module.path} label={module.label} />
|
||||
@@ -121,13 +119,15 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
</Paper>
|
||||
</AppBar>
|
||||
|
||||
{/* Contenido del Módulo (renderizado por <Outlet /> en AppRoutes) */}
|
||||
{/* Contenido del Módulo */}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3, // Padding
|
||||
// overflowY: 'auto' // Si el contenido del módulo es muy largo
|
||||
py: isReportesModule ? 0 : 3, // Padding vertical condicional. Si es el módulo de Reportes, px es 0 si no 3
|
||||
px: isReportesModule ? 0 : 3, // Padding horizontal condicional. Si es el módulo de Reportes, px es 0 si no 3
|
||||
display: 'flex', // IMPORTANTE: Para que el hijo (ReportesIndexPage) pueda usar height: '100%'
|
||||
flexDirection: 'column' // IMPORTANTE
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -139,13 +139,10 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Modal para cambio de clave opcional (no forzado) */}
|
||||
{/* Si showForcedPasswordChangeModal es true pero isPasswordChangeForced es false,
|
||||
se mostrará aquí también. */}
|
||||
<ChangePasswordModal
|
||||
open={showForcedPasswordChangeModal}
|
||||
onClose={handleModalClose}
|
||||
isFirstLogin={isPasswordChangeForced} // Esto controla el comportamiento del modal
|
||||
isFirstLogin={isPasswordChangeForced}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ListadoDistribucionDistSimpleDto } from './ListadoDistribucionDistSimpleDto';
|
||||
import type { ListadoDistribucionDistPromedioDiaDto } from './ListadoDistribucionDistPromedioDiaDto';
|
||||
|
||||
export interface ListadoDistribucionDistribuidoresResponseDto {
|
||||
detalleSimple: ListadoDistribucionDistSimpleDto[];
|
||||
promediosPorDia: ListadoDistribucionDistPromedioDiaDto[];
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody,
|
||||
TableFooter
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import type {Theme} from '@mui/material/styles';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
|
||||
import SeleccionaReporteComparativaConsumoBobinas from './SeleccionaReporteComparativaConsumoBobinas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaz extendida para DataGrid
|
||||
interface ComparativaConsumoBobinasDataGridDto extends ComparativaConsumoBobinasDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDto[]>([]);
|
||||
const [reportData, setReportData] = useState<ComparativaConsumoBobinasDataGridDto[]>([]); // Usar tipo extendido
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -21,9 +27,14 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
fechaInicioMesA: string; fechaFinMesA: string;
|
||||
fechaInicioMesB: string; fechaFinMesB: string;
|
||||
idPlanta?: number | null; consolidado: boolean;
|
||||
nombrePlanta?: string; // Para el PDF
|
||||
nombrePlanta?: string;
|
||||
mesA?: string;
|
||||
mesB?: string;
|
||||
} | null>(null);
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaInicioMesA: string; fechaFinMesA: string;
|
||||
fechaInicioMesB: string; fechaFinMesB: string;
|
||||
@@ -40,12 +51,24 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
const plantaData = await plantaService.getPlantaById(params.idPlanta);
|
||||
plantaNombre = plantaData?.nombre ?? "N/A";
|
||||
}
|
||||
setCurrentParams({...params, nombrePlanta: plantaNombre});
|
||||
// Formatear nombres de meses para el PDF
|
||||
const formatMonthYear = (dateString: string) => {
|
||||
const date = new Date(dateString + 'T00:00:00'); // Asegurar que se parsea como local
|
||||
return date.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
||||
};
|
||||
|
||||
setCurrentParams({
|
||||
...params,
|
||||
nombrePlanta: plantaNombre,
|
||||
mesA: formatMonthYear(params.fechaInicioMesA),
|
||||
mesB: formatMonthYear(params.fechaInicioMesB)
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await reportesService.getComparativaConsumoBobinas(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.tipoBobina}-${index}` }));
|
||||
setReportData(dataWithIds);
|
||||
if (dataWithIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -72,7 +95,7 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const dataToExport = reportData.map(item => ({
|
||||
const dataToExport = reportData.map(({ ...rest }) => rest).map(item => ({
|
||||
"Tipo Bobina": item.tipoBobina,
|
||||
"Cant. Mes A": item.bobinasUtilizadasMesA,
|
||||
"Cant. Mes B": item.bobinasUtilizadasMesB,
|
||||
@@ -82,7 +105,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
"Dif. Kg": item.diferenciaKilosUtilizados,
|
||||
}));
|
||||
|
||||
// Totales
|
||||
const totales = dataToExport.reduce((acc, row) => {
|
||||
acc.cantA += Number(row["Cant. Mes A"]); acc.cantB += Number(row["Cant. Mes B"]);
|
||||
acc.difCant += Number(row["Dif. Cant."]); acc.kgA += Number(row["Kg Mes A"]);
|
||||
@@ -96,7 +118,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
"Dif. Kg": totales.difKg,
|
||||
});
|
||||
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
const headers = Object.keys(dataToExport[0] || {});
|
||||
ws['!cols'] = headers.map(h => {
|
||||
@@ -104,7 +125,6 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ComparativaConsumo");
|
||||
let fileName = "ReporteComparativaConsumoBobinas";
|
||||
@@ -141,6 +161,118 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const columns: GridColDef<ComparativaConsumoBobinasDataGridDto>[] = [ // Tipar con la interfaz correcta
|
||||
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 250, flex: 1.5 },
|
||||
{ field: 'bobinasUtilizadasMesA', headerName: 'Cant. Mes A', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'bobinasUtilizadasMesB', headerName: 'Cant. Mes B', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'diferenciaBobinasUtilizadas', headerName: 'Dif. Cant.', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosUtilizadosMesA', headerName: 'Kg Mes A', type: 'number', width: 120, align: 'right', headerAlign: 'right', cellClassName: 'separator-left', headerClassName: 'separator-left', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosUtilizadosMesB', headerName: 'Kg Mes B', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'diferenciaKilosUtilizados', headerName: 'Dif. Kg', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const rows = useMemo(() => reportData, [reportData]);
|
||||
|
||||
// Calcular totales para el footer
|
||||
const totalesGenerales = useMemo(() => {
|
||||
if (reportData.length === 0) return null;
|
||||
return {
|
||||
bobinasUtilizadasMesA: reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesA, 0),
|
||||
bobinasUtilizadasMesB: reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesB, 0),
|
||||
diferenciaBobinasUtilizadas: reportData.reduce((sum, item) => sum + item.diferenciaBobinasUtilizadas, 0),
|
||||
kilosUtilizadosMesA: reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesA, 0),
|
||||
kilosUtilizadosMesB: reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesB, 0),
|
||||
diferenciaKilosUtilizados: reportData.reduce((sum, item) => sum + item.diferenciaKilosUtilizados, 0),
|
||||
};
|
||||
}, [reportData]);
|
||||
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooter = () => {
|
||||
if (!totalesGenerales) return null;
|
||||
|
||||
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
|
||||
const colConfig = columns.find(c => c.field === field);
|
||||
// Ajustar anchos para los totales para que sean más compactos
|
||||
let targetWidth: number | string = 'auto';
|
||||
let targetMinWidth: number | string = 'auto';
|
||||
|
||||
if (isLabel) {
|
||||
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.5) : 100; // Más corto para "TOTALES:"
|
||||
targetMinWidth = 80;
|
||||
} else if (colConfig) {
|
||||
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.75) : 90; // 75% del ancho de columna, mínimo 70
|
||||
targetMinWidth = 70;
|
||||
}
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
minWidth: targetMinWidth,
|
||||
width: targetWidth,
|
||||
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
|
||||
paddingRight: isLabel ? 1 : (field === 'diferenciaKilosUtilizados' ? 0 : 1), // pr en theme units
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
// Aplicar el separador si es la columna 'kilosUtilizadosMesA'
|
||||
if (field === 'kilosUtilizadosMesA') {
|
||||
style.borderLeft = `2px solid grey`; // O theme.palette.divider
|
||||
style.paddingLeft = '8px'; // Espacio después del separador
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px',
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
px:1,
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none',
|
||||
width: 'auto',
|
||||
'& .MuiToolbar-root': {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
px:1,
|
||||
flexShrink: 1,
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesA')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesA)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasUtilizadasMesB')}>{numberLocaleFormatter(totalesGenerales.bobinasUtilizadasMesB)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('diferenciaBobinasUtilizadas')}>{numberLocaleFormatter(totalesGenerales.diferenciaBobinasUtilizadas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesA')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesA)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosUtilizadosMesB')}>{numberLocaleFormatter(totalesGenerales.kilosUtilizadosMesB)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('diferenciaKilosUtilizados')}>{numberLocaleFormatter(totalesGenerales.diferenciaKilosUtilizados)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -173,51 +305,36 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Bobina</TableCell>
|
||||
<TableCell align="right">Cant. Mes A</TableCell>
|
||||
<TableCell align="right">Cant. Mes B</TableCell>
|
||||
<TableCell align="right">Dif. Cant.</TableCell>
|
||||
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>Kg Mes A</TableCell>
|
||||
<TableCell align="right">Kg Mes B</TableCell>
|
||||
<TableCell align="right">Dif. Kg</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.map((row, idx) => (
|
||||
<TableRow key={`${row.tipoBobina}-${idx}`}>
|
||||
<TableCell>{row.tipoBobina}</TableCell>
|
||||
<TableCell align="right">{row.bobinasUtilizadasMesA.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.bobinasUtilizadasMesB.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.diferenciaBobinasUtilizadas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{ borderLeft: '2px solid grey' }}>{row.kilosUtilizadosMesA.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosUtilizadosMesB.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.diferenciaKilosUtilizados.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow sx={{backgroundColor: 'grey.300'}}>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>TOTALES:</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesA, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.bobinasUtilizadasMesB, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.diferenciaBobinasUtilizadas, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem', borderLeft: '2px solid grey'}}>{reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesA, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.kilosUtilizadosMesB, 0).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{reportData.reduce((sum, item) => sum + item.diferenciaKilosUtilizados, 0).toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Paper sx={{
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
'& .separator-left': {
|
||||
borderLeft: (theme: Theme) => `2px solid ${theme.palette.divider}`,
|
||||
},
|
||||
}}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: CustomFooter }}
|
||||
density="compact"
|
||||
sx={{ height: 'calc(100vh - 300px)' }} // Ajusta esta altura según sea necesario
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: { pageSize: 10, page: 0 },
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[5, 10, 25, 50]}
|
||||
disableRowSelectionOnClick
|
||||
// hideFooterSelectedRowCount // Ya se maneja en CustomFooter
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody, TableFooter
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
DataGrid,
|
||||
type GridColDef,
|
||||
GridFooterContainer,
|
||||
GridFooter
|
||||
} from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto';
|
||||
import SeleccionaReporteConsumoBobinasPublicacion from './SeleccionaReporteConsumoBobinasPublicacion';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Helper para agrupar los datos por Planta
|
||||
const groupDataByPlanta = (data: ConsumoBobinasPublicacionDto[]) => {
|
||||
return data.reduce((acc, current) => {
|
||||
const plantaKey = current.nombrePlanta;
|
||||
if (!acc[plantaKey]) {
|
||||
acc[plantaKey] = [];
|
||||
}
|
||||
acc[plantaKey].push(current);
|
||||
return acc;
|
||||
}, {} as Record<string, ConsumoBobinasPublicacionDto[]>);
|
||||
};
|
||||
|
||||
const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ConsumoBobinasPublicacionDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -31,6 +25,7 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
nombrePublicacion?: string; // Mantenido para nombre de archivo, no afecta al título DataGrid
|
||||
} | null>(null);
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
@@ -45,8 +40,10 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
const data = await reportesService.getConsumoBobinasPorPublicacion(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.nombrePlanta}-${item.nombrePublicacion}-${index}` }));
|
||||
setReportData(dataWithIds);
|
||||
|
||||
if (dataWithIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -73,24 +70,24 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const dataToExport = reportData.map(item => ({
|
||||
"Planta": item.nombrePlanta,
|
||||
"Publicación": item.nombrePublicacion,
|
||||
"Total Kilos": item.totalKilos,
|
||||
"Cantidad Bobinas": item.cantidadBobinas,
|
||||
const dataToExport = reportData.map(({ ...rest }) => ({
|
||||
"Planta": rest.nombrePlanta,
|
||||
"Publicación": rest.nombrePublicacion,
|
||||
"Total Kilos": rest.totalKilos,
|
||||
"Cantidad Bobinas": rest.cantidadBobinas,
|
||||
}));
|
||||
|
||||
// Calcular totales generales
|
||||
const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0);
|
||||
const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0);
|
||||
|
||||
dataToExport.push({
|
||||
"Planta": "TOTAL GENERAL",
|
||||
"Publicación": "",
|
||||
"Publicación": "", // Celda vacía para alineación
|
||||
"Total Kilos": totalGeneralKilos,
|
||||
"Cantidad Bobinas": totalGeneralBobinas,
|
||||
});
|
||||
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
const headers = Object.keys(dataToExport[0] || {});
|
||||
ws['!cols'] = headers.map(h => {
|
||||
@@ -117,7 +114,7 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getConsumoBobinasPorPublicacionPdf(currentParams);
|
||||
const blob = await reportesService.getConsumoBobinasPorPublicacionPdf(currentParams); // solo fechaDesde, fechaHasta
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
@@ -134,10 +131,100 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const groupedData = reportData.length > 0 ? groupDataByPlanta(reportData) : {};
|
||||
const totalGeneralKilos = reportData.reduce((sum, item) => sum + item.totalKilos, 0);
|
||||
const totalGeneralBobinas = reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0);
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'nombrePlanta', headerName: 'Planta', width: 200, flex: 1 },
|
||||
{ field: 'nombrePublicacion', headerName: 'Publicación', width: 250, flex: 1.5 },
|
||||
{
|
||||
field: 'totalKilos',
|
||||
headerName: 'Total Kilos',
|
||||
type: 'number',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '',
|
||||
flex: 0.8
|
||||
},
|
||||
{
|
||||
field: 'cantidadBobinas',
|
||||
headerName: 'Cant. Bobinas',
|
||||
type: 'number',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '',
|
||||
flex: 0.8
|
||||
},
|
||||
];
|
||||
|
||||
const rows = useMemo(() => reportData, [reportData]);
|
||||
|
||||
const totalGeneralKilos = useMemo(() =>
|
||||
reportData.reduce((sum, item) => sum + item.totalKilos, 0),
|
||||
[reportData]);
|
||||
const totalGeneralBobinas = useMemo(() =>
|
||||
reportData.reduce((sum, item) => sum + item.cantidadBobinas, 0),
|
||||
[reportData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooter = () => {
|
||||
// Podrías añadir una condición para no renderizar si no hay datos/totales
|
||||
// if (totalGeneralKilos === 0 && totalGeneralBobinas === 0) return null;
|
||||
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between', // Separa la paginación (izquierda) de los totales (derecha)
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px', // Altura estándar para el footer
|
||||
// No es necesario p: aquí si los hijos lo manejan o el GridFooterContainer lo aplica por defecto
|
||||
}}>
|
||||
{/* Box para la paginación estándar */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0, // Evita que este box se encoja si los totales son anchos
|
||||
overflow: 'hidden', // Para asegurar que no desborde si el contenido interno es muy ancho
|
||||
px: 1, // Padding horizontal para el contenedor de la paginación
|
||||
// Considera un flexGrow o un width/maxWidth si necesitas más control sobre el espacio de la paginación
|
||||
// Ejemplo: flexGrow: 1, maxWidth: 'calc(100% - 250px)' (para dejar espacio a los totales)
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none', // Quitar el borde superior del GridFooter interno
|
||||
width: 'auto', // Permite que el GridFooter se ajuste a su contenido (paginador)
|
||||
'& .MuiToolbar-root': { // Ajustar padding del toolbar de paginación
|
||||
paddingLeft: 0, // O un valor pequeño si es necesario
|
||||
paddingRight: 0,
|
||||
},
|
||||
// Mantenemos oculto el contador de filas seleccionadas si no lo queremos
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Box para los totales personalizados */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap', // Evita que los totales hagan salto de línea
|
||||
overflowX: 'auto', // Scroll DENTRO de este Box si los totales son muy anchos
|
||||
px: 2, // Padding horizontal para el contenedor de los totales (ajusta pr:2 de tu ejemplo)
|
||||
flexShrink: 1, // Permitir que este contenedor se encoja si la paginación necesita más espacio
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ mr: 1, fontWeight: 'bold' }}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ mr: 2, minWidth: '80px', textAlign: 'right', fontWeight: 'bold' }}>
|
||||
Kg: {totalGeneralKilos.toLocaleString('es-AR')}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: '80px', textAlign: 'right', fontWeight: 'bold' }}>
|
||||
Cant: {totalGeneralBobinas.toLocaleString('es-AR')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
@@ -171,55 +258,33 @@ const ReporteConsumoBobinasPublicacionPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Planta</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Publicación</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Total Kilos</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Cant. Bobinas</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(groupedData).map(([plantaKey, publicaciones]) => {
|
||||
const totalKilosPlanta = publicaciones.reduce((sum, item) => sum + item.totalKilos, 0);
|
||||
const totalBobinasPlanta = publicaciones.reduce((sum, item) => sum + item.cantidadBobinas, 0);
|
||||
return (
|
||||
<React.Fragment key={plantaKey}>
|
||||
<TableRow sx={{ backgroundColor: 'rgba(0, 0, 0, 0.08)' }}>
|
||||
<TableCell colSpan={4} sx={{ fontWeight: 'bold' }}>{plantaKey}</TableCell>
|
||||
</TableRow>
|
||||
{publicaciones.map((pub, pubIdx) => (
|
||||
<TableRow key={`${plantaKey}-${pubIdx}`}>
|
||||
<TableCell></TableCell> {/* Columna Planta vacía para esta fila */}
|
||||
<TableCell>{pub.nombrePublicacion}</TableCell>
|
||||
<TableCell align="right">{pub.totalKilos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{pub.cantidadBobinas.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{/* Fila de totales por planta */}
|
||||
<TableRow sx={{ backgroundColor: 'rgba(0, 0, 0, 0.04)'}}>
|
||||
<TableCell colSpan={2} align="right" sx={{ fontWeight: 'bold', fontStyle:'italic' }}>Total Planta ({plantaKey}):</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', fontStyle:'italic' }}>{totalKilosPlanta.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', fontStyle:'italic' }}>{totalBobinasPlanta.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
)})}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow sx={{backgroundColor: 'grey.300'}}>
|
||||
<TableCell colSpan={2} align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>TOTAL GENERAL:</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{totalGeneralKilos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight: 'bold', fontSize: '1.1rem'}}>{totalGeneralBobinas.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Paper sx={{
|
||||
height: 'calc(100vh - 380px)', // Ajustar altura para dar espacio al footer
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
'& .MuiDataGrid-footerContainer': { // Asegurar que el contenedor del footer tenga suficiente espacio
|
||||
minHeight: '52px', // o el alto que necesite tu CustomFooter
|
||||
}
|
||||
}}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: CustomFooter }}
|
||||
density="compact"
|
||||
// pageSizeOptions={[10, 25, 50]} // Descomentar si deseas que el usuario cambie el tamaño de página
|
||||
// initialState={{
|
||||
// pagination: {
|
||||
// paginationModel: { pageSize: 25, page: 0 },
|
||||
// },
|
||||
// }}
|
||||
// autoHeight // Si se usa autoHeight, el `height` del Paper no aplicará scroll a la tabla
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
|
||||
388
Frontend/src/pages/Reportes/ReporteControlDevolucionesPage.tsx
Normal file
388
Frontend/src/pages/Reportes/ReporteControlDevolucionesPage.tsx
Normal 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;
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto';
|
||||
import type { BalanceCuentaDistDto } from '../../models/dtos/Reportes/BalanceCuentaDistDto';
|
||||
@@ -12,22 +13,214 @@ import SeleccionaReporteCuentasDistribuidores from './SeleccionaReporteCuentasDi
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
type MovimientoConSaldo = BalanceCuentaDistDto & { id: string; saldoAcumulado: number };
|
||||
type NotaConSaldo = BalanceCuentaDebCredDto & { id: string; saldoAcumulado: number };
|
||||
type PagoConSaldo = BalanceCuentaPagosDto & { id: string; saldoAcumulado: number };
|
||||
|
||||
const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [originalReportData, setOriginalReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
|
||||
const [movimientosConSaldo, setMovimientosConSaldo] = useState<MovimientoConSaldo[]>([]);
|
||||
const [notasConSaldo, setNotasConSaldo] = useState<NotaConSaldo[]>([]);
|
||||
const [pagosConSaldo, setPagosConSaldo] = useState<PagoConSaldo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState<boolean>(false);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [showParamSelector, setShowParamSelector] = useState<boolean>(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
idDistribuidor: number;
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
nombreDistribuidor?: string; // Para el PDF y nombre de archivo
|
||||
nombreEmpresa?: string; // Para el PDF y nombre de archivo
|
||||
nombreDistribuidor?: string;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
|
||||
// Calcula saldos acumulados seccion por seccion
|
||||
const calcularSaldosPorSeccion = (data: ReporteCuentasDistribuidorResponseDto) => {
|
||||
const procesarLista = <T extends { fecha: string | Date; debe?: number; haber?: number; id?: number | string }>(
|
||||
lista: T[],
|
||||
idPrefix: string,
|
||||
saldoInicial: number
|
||||
): Array<T & { id: string; saldoAcumulado: number }> => {
|
||||
let acumulado = saldoInicial;
|
||||
return lista
|
||||
.slice()
|
||||
.sort((a, b) => new Date(a.fecha).getTime() - new Date(b.fecha).getTime())
|
||||
.map((item, idx) => {
|
||||
const debe = Number(item.debe) || 0;
|
||||
const haber = Number(item.haber) || 0;
|
||||
acumulado += debe - haber;
|
||||
return {
|
||||
...item,
|
||||
id: item.id?.toString() || `${idPrefix}-${idx}`,
|
||||
saldoAcumulado: acumulado
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const movs = procesarLista(data.entradasSalidas, 'mov', 0);
|
||||
const ultimoMov = movs.length ? movs[movs.length - 1].saldoAcumulado : 0;
|
||||
const notas = procesarLista(data.debitosCreditos, 'nota', ultimoMov);
|
||||
const ultimoNota = notas.length ? notas[notas.length - 1].saldoAcumulado : ultimoMov;
|
||||
const pagos = procesarLista(data.pagos, 'pago', ultimoNota);
|
||||
|
||||
return { movs, notas, pagos };
|
||||
};
|
||||
|
||||
// Genera columnas con formateo
|
||||
const generarColumns = (): { mov: GridColDef[]; notas: GridColDef[]; pagos: GridColDef[] } => ({
|
||||
mov: [
|
||||
{ field: 'fecha', headerName: 'Fecha', width: 100 },
|
||||
{ field: 'publicacion', headerName: 'Publicación', flex: 1 },
|
||||
{ field: 'remito', headerName: 'Remito', width: 100 },
|
||||
{
|
||||
field: 'debe',
|
||||
headerName: 'Debe',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'debe', headerName: 'Debe', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
|
||||
{
|
||||
field: 'haber',
|
||||
headerName: 'Haber',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'haber', headerName: 'Haber', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
|
||||
{
|
||||
field: 'saldoAcumulado',
|
||||
headerName: 'Saldo',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'saldoAcumulado', headerName: 'Saldo', type: 'number', width: 150, align: 'right', headerAlign: 'right' }
|
||||
],
|
||||
notas: [
|
||||
{ field: 'fecha', headerName: 'Fecha', width: 100 },
|
||||
{ field: 'referencia', headerName: 'Referencia', flex: 1 },
|
||||
{
|
||||
field: 'debe',
|
||||
headerName: 'Debe',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'debe', headerName: 'Debe', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
|
||||
{
|
||||
field: 'haber',
|
||||
headerName: 'Haber',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'haber', headerName: 'Haber', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
|
||||
{
|
||||
field: 'saldoAcumulado',
|
||||
headerName: 'Saldo',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'saldoAcumulado', headerName: 'Saldo', type: 'number', width: 150, align: 'right', headerAlign: 'right' }
|
||||
],
|
||||
pagos: [
|
||||
{ field: 'fecha', headerName: 'Fecha', width: 100 },
|
||||
{ field: 'recibo', headerName: 'Recibo', width: 100 },
|
||||
{ field: 'tipo', headerName: 'Tipo', width: 150 },
|
||||
{ field: 'detalle', headerName: 'Detalle', flex: 1 },
|
||||
{
|
||||
field: 'debe',
|
||||
headerName: 'Debe',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'debe', headerName: 'Debe', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
|
||||
{
|
||||
field: 'haber',
|
||||
headerName: 'Haber',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'haber', headerName: 'Haber', type: 'number', width: 130, align: 'right', headerAlign: 'right' },
|
||||
{
|
||||
field: 'saldoAcumulado',
|
||||
headerName: 'Saldo',
|
||||
type: 'number',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
valueFormatter: (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : ''
|
||||
},
|
||||
//{ field: 'saldoAcumulado', headerName: 'Saldo', type: 'number', width: 150, align: 'right', headerAlign: 'right' }
|
||||
]
|
||||
});
|
||||
|
||||
// Renderiza DataGrid con footer de totales
|
||||
const renderDataGrid = (
|
||||
rows: Array<{ debe?: number; haber?: number }>,
|
||||
columns: GridColDef[]
|
||||
) => {
|
||||
const totalDebe = rows.reduce((sum, r) => sum + (r.debe || 0), 0);
|
||||
const totalHaber = rows.reduce((sum, r) => sum + (r.haber || 0), 0);
|
||||
return rows.length ? (
|
||||
<Paper sx={{ height: 350, width: '100%', mb: 2 }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{
|
||||
footer: () => (
|
||||
<GridFooterContainer>
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
<Box sx={{ p: 1, fontWeight: 'bold' }}>
|
||||
<Typography variant="subtitle2" component="span" sx={{ mr: 2 }}>
|
||||
TOTA DEBE: <strong>{totalDebe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="span">
|
||||
TOTAL HABER: <strong>{totalHaber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{ fontStyle: 'italic', mb: 2 }}>No hay registros</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
idDistribuidor: number;
|
||||
idEmpresa: number;
|
||||
@@ -35,16 +228,18 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setReportData(null);
|
||||
|
||||
// Obtener nombres para el PDF/Excel
|
||||
const distService = (await import('../../services/Distribucion/distribuidorService')).default;
|
||||
const distData = await distService.getDistribuidorById(params.idDistribuidor);
|
||||
const empService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empService.getEmpresaById(params.idEmpresa);
|
||||
setOriginalReportData(null);
|
||||
setMovimientosConSaldo([]);
|
||||
setNotasConSaldo([]);
|
||||
setPagosConSaldo([]);
|
||||
|
||||
const distSvc = (await import('../../services/Distribucion/distribuidorService')).default;
|
||||
const empSvc = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const [distData, empData] = await Promise.all([
|
||||
distSvc.getDistribuidorById(params.idDistribuidor),
|
||||
empSvc.getEmpresaById(params.idEmpresa)
|
||||
]);
|
||||
setCurrentParams({
|
||||
...params,
|
||||
nombreDistribuidor: distData?.nombre,
|
||||
@@ -53,17 +248,17 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
const data = await reportesService.getReporteCuentasDistribuidor(params);
|
||||
setReportData(data);
|
||||
const noData = (!data.entradasSalidas?.length && !data.debitosCreditos?.length && !data.pagos?.length && !data.saldos?.length);
|
||||
if (noData) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setOriginalReportData(data);
|
||||
const { movs, notas, pagos } = calcularSaldosPorSeccion(data);
|
||||
setMovimientosConSaldo(movs);
|
||||
setNotasConSaldo(notas);
|
||||
setPagosConSaldo(pagos);
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
} catch (error: any) {
|
||||
const msg = axios.isAxiosError(error) && error.response?.data?.message
|
||||
? error.response.data.message
|
||||
: 'Error al generar el reporte';
|
||||
setApiErrorParams(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -71,166 +266,46 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
setReportData(null);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
setOriginalReportData(null);
|
||||
setMovimientosConSaldo([]);
|
||||
setNotasConSaldo([]);
|
||||
setPagosConSaldo([]);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!reportData) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
if (!originalReportData) return;
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
if (reportData.saldos?.length) {
|
||||
const saldosToExport = reportData.saldos.map(item => ({ "Saldo Actual": item.monto }));
|
||||
const wsSaldos = XLSX.utils.json_to_sheet(saldosToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsSaldos, "SaldoActual");
|
||||
if (movimientosConSaldo.length) {
|
||||
const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
|
||||
}
|
||||
if (reportData.entradasSalidas?.length) {
|
||||
const esToExport = reportData.entradasSalidas.map(item => ({
|
||||
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }),
|
||||
"Publicación": item.publicacion, "Remito": item.remito, "Cantidad": item.cantidad,
|
||||
"Observación": item.observacion, "Debe": item.debe, "Haber": item.haber,
|
||||
}));
|
||||
const wsES = XLSX.utils.json_to_sheet(esToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsES, "EntradasSalidas");
|
||||
if (notasConSaldo.length) {
|
||||
const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Notas');
|
||||
}
|
||||
if (reportData.debitosCreditos?.length) {
|
||||
const dcToExport = reportData.debitosCreditos.map(item => ({
|
||||
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }),
|
||||
"Referencia": item.referencia, "Debe": item.debe, "Haber": item.haber,
|
||||
}));
|
||||
const wsDC = XLSX.utils.json_to_sheet(dcToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsDC, "DebitosCreditos");
|
||||
if (pagosConSaldo.length) {
|
||||
const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
|
||||
}
|
||||
if (reportData.pagos?.length) {
|
||||
const paToExport = reportData.pagos.map(item => ({
|
||||
"Fecha": new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }),
|
||||
"Recibo": item.recibo, "Tipo": item.tipo, "Debe": item.debe, "Haber": item.haber,
|
||||
"Detalle": item.detalle,
|
||||
}));
|
||||
const wsPA = XLSX.utils.json_to_sheet(paToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsPA, "Pagos");
|
||||
}
|
||||
|
||||
let fileName = "ReporteCuentaDistribuidor";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreDistribuidor?.replace(/\s+/g, '') ?? `Dist${currentParams.idDistribuidor}`}`;
|
||||
fileName += `_Emp${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? currentParams.idEmpresa}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [reportData, currentParams]);
|
||||
XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
|
||||
}, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
if (!currentParams) {
|
||||
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
|
||||
return;
|
||||
}
|
||||
if (!currentParams) return;
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getReporteCuentasDistribuidorPdf(currentParams);
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
if (!w) alert("Permite popups para ver el PDF.");
|
||||
}
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} catch {
|
||||
setError('Ocurrió un error al generar el PDF.');
|
||||
/* manejar error */
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
// --- Funciones para renderizar las tablas ---
|
||||
const renderEntradasSalidasTable = (data: BalanceCuentaDistDto[]) => (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Publicación</TableCell>
|
||||
<TableCell>Remito</TableCell><TableCell align="right">Cantidad</TableCell>
|
||||
<TableCell>Observación</TableCell>
|
||||
<TableCell align="right">Debe</TableCell><TableCell align="right">Haber</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row, idx) => (
|
||||
<TableRow key={`es-${idx}`}>
|
||||
<TableCell>{new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' })}</TableCell>
|
||||
<TableCell>{row.publicacion}</TableCell><TableCell>{row.remito}</TableCell>
|
||||
<TableCell align="right">{row.cantidad.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell>{row.observacion}</TableCell>
|
||||
<TableCell align="right">{row.debe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
<TableCell align="right">{row.haber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
const renderDebitosCreditosTable = (data: BalanceCuentaDebCredDto[]) => (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Referencia</TableCell>
|
||||
<TableCell align="right">Debe</TableCell><TableCell align="right">Haber</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row, idx) => (
|
||||
<TableRow key={`dc-${idx}`}>
|
||||
<TableCell>{new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' })}</TableCell>
|
||||
<TableCell>{row.referencia}</TableCell>
|
||||
<TableCell align="right">{row.debe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
<TableCell align="right">{row.haber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
const renderPagosTable = (data: BalanceCuentaPagosDto[]) => (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Recibo</TableCell><TableCell>Tipo</TableCell>
|
||||
<TableCell align="right">Debe</TableCell><TableCell align="right">Haber</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row, idx) => (
|
||||
<TableRow key={`pa-${idx}`}>
|
||||
<TableCell>{new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' })}</TableCell>
|
||||
<TableCell>{row.recibo}</TableCell><TableCell>{row.tipo}</TableCell>
|
||||
<TableCell align="right">{row.debe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
<TableCell align="right">{row.haber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
<TableCell>{row.detalle}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteCuentasDistribuidores
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
@@ -243,15 +318,23 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Totales de cada sección para resumen final
|
||||
const totalMov = movimientosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||
const totalNot = notasConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||
const totalPag = pagosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||
const saldoInicial = originalReportData?.saldos?.[0]?.monto || 0;
|
||||
|
||||
const cols = generarColumns();
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Cuenta Corriente Distribuidor</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h5">Cuenta Corriente Distribuidor</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={!originalReportData} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} /> : 'Abrir PDF'}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={!originalReportData} size="small">
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
@@ -260,33 +343,31 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography>Distribuidor: <strong>{currentParams?.nombreDistribuidor}</strong></Typography>
|
||||
<Typography>Empresa: <strong>{currentParams?.nombreEmpresa}</strong></Typography>
|
||||
<Typography>Período: <strong>{currentParams?.fechaDesde}</strong> al <strong>{currentParams?.fechaHasta}</strong></Typography>
|
||||
<Typography>Saldo a la Fecha {new Date().toLocaleDateString('es-AR')}: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
||||
</Paper>
|
||||
|
||||
{!loading && !error && reportData && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom>Saldo Actual</Typography>
|
||||
{reportData.saldos && reportData.saldos.length > 0 ? (
|
||||
<Typography sx={{ mb: 2 }}>
|
||||
{reportData.saldos[0].monto.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>Movimientos de Entrada / Salida</Typography>
|
||||
{renderDataGrid(movimientosConSaldo, cols.mov)}
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>Notas de Crédito / Débito</Typography>
|
||||
{renderDataGrid(notasConSaldo, cols.notas)}
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>Pagos Recibidos / Realizados</Typography>
|
||||
{renderDataGrid(pagosConSaldo, cols.pagos)}
|
||||
|
||||
<Paper sx={{ p: 2, mt: 3 }}>
|
||||
<Typography variant="h6">Resumen Final</Typography>
|
||||
<Typography>Movimientos (Debe - Haber): {totalMov.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||
<Typography>Notas C/D (Debe - Haber): {totalNot.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||
<Typography>Pagos (Debe - Haber): {totalPag.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mt: 1 }}>
|
||||
Saldo Final del Período: {(totalMov + totalNot + totalPag).toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
) : <Typography sx={{ mb: 2, fontStyle: 'italic' }}>No hay saldo actual disponible.</Typography>}
|
||||
|
||||
<Typography variant="h6" gutterBottom>Movimientos (Entradas/Salidas)</Typography>
|
||||
{reportData.entradasSalidas && reportData.entradasSalidas.length > 0 ?
|
||||
renderEntradasSalidasTable(reportData.entradasSalidas) : <Typography sx={{ fontStyle: 'italic' }}>No hay movimientos de entradas/salidas.</Typography>}
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Notas de Débito/Crédito</Typography>
|
||||
{reportData.debitosCreditos && reportData.debitosCreditos.length > 0 ?
|
||||
renderDebitosCreditosTable(reportData.debitosCreditos) : <Typography sx={{ fontStyle: 'italic' }}>No hay notas de débito/crédito.</Typography>}
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Pagos</Typography>
|
||||
{reportData.pagos && reportData.pagos.length > 0 ?
|
||||
renderPagosTable(reportData.pagos) : <Typography sx={{ fontStyle: 'italic' }}>No hay pagos registrados.</Typography>}
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && (!reportData || (!reportData.entradasSalidas?.length && !reportData.debitosCreditos?.length && !reportData.pagos?.length && !reportData.saldos?.length)) &&
|
||||
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,76 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo, type JSXElementConstructor, type HTMLAttributes } from 'react'; // Añadido JSXElementConstructor, HTMLAttributes
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme // Añadido SxProps, Theme
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter, type GridSlotsComponent } from '@mui/x-data-grid'; // Añadido GridSlotsComponent
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
|
||||
import type { DetalleDistribucionCanillaDto } from '../../models/dtos/Reportes/DetalleDistribucionCanillaDto';
|
||||
import type { DetalleDistribucionCanillaAllDto } from '../../models/dtos/Reportes/DetalleDistribucionCanillaAllDto';
|
||||
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Para el tipo del footer en DataGridSectionProps
|
||||
type FooterPropsOverrides = {}; // Puedes extender esto si tus footers tienen props específicos
|
||||
type CustomFooterType = JSXElementConstructor<HTMLAttributes<HTMLDivElement> & { sx?: SxProps<Theme> } & FooterPropsOverrides>;
|
||||
|
||||
|
||||
interface TotalesComunes {
|
||||
totalCantSalida: number;
|
||||
totalCantEntrada: number;
|
||||
vendidos: number;
|
||||
totalRendir: number;
|
||||
}
|
||||
|
||||
interface DataGridSectionProps {
|
||||
title: string;
|
||||
data: Array<any>;
|
||||
columns: GridColDef[];
|
||||
isLoading?: boolean;
|
||||
footerComponent?: CustomFooterType; // Tipo más específico para el footer
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const DataGridSection: React.FC<DataGridSectionProps> = ({ title, data, columns, isLoading, footerComponent, height = 300 }) => {
|
||||
const rows = useMemo(() => data.map((r, i) => ({ ...r, _internalId: i })), [data]);
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||
}
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return <Typography sx={{ mt: 1, fontStyle: 'italic', mb:2 }}>No hay datos para {title.toLowerCase()}.</Typography>;
|
||||
}
|
||||
|
||||
const slotsProp: Partial<GridSlotsComponent> = {};
|
||||
if (footerComponent) {
|
||||
slotsProp.footer = footerComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2, fontWeight: 'bold' }}>{title}</Typography>
|
||||
<Paper sx={{ height: footerComponent ? 'auto' : height, width: '100%', mb: 2, '& .MuiDataGrid-footerContainer': { minHeight: footerComponent ? '52px' : undefined} }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
getRowId={(row) => row.id || row._internalId}
|
||||
slots={slotsProp} // Usar el objeto slotsProp
|
||||
hideFooterSelectedRowCount={!!footerComponent}
|
||||
autoHeight={!!footerComponent}
|
||||
sx={!footerComponent ? {} : {
|
||||
'& .MuiTablePagination-root': { display: 'none' },
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -23,8 +83,34 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
idEmpresa: number;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
const [pdfSoloTotales, setPdfSoloTotales] = useState(false); // Estado para el tipo de PDF
|
||||
const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
|
||||
|
||||
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
|
||||
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesAccionistas, setTotalesAccionistas] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState<TotalesComunes>(initialTotals);
|
||||
|
||||
|
||||
const currencyFormatter = (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
|
||||
const numberFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const calculateAndSetTotals = (dataArray: Array<any> | undefined, setTotalsFunc: React.Dispatch<React.SetStateAction<TotalesComunes>>) => {
|
||||
if (dataArray && dataArray.length > 0) {
|
||||
const totals = dataArray.reduce((acc, item) => {
|
||||
acc.totalCantSalida += Number(item.totalCantSalida) || 0;
|
||||
acc.totalCantEntrada += Number(item.totalCantEntrada) || 0;
|
||||
acc.totalRendir += Number(item.totalRendir) || 0;
|
||||
return acc;
|
||||
}, { totalCantSalida: 0, totalCantEntrada: 0, totalRendir: 0 });
|
||||
totals.vendidos = totals.totalCantSalida - totals.totalCantEntrada;
|
||||
setTotalsFunc(totals);
|
||||
} else {
|
||||
setTotalsFunc(initialTotals);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fecha: string;
|
||||
@@ -33,29 +119,49 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
|
||||
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empresaService.getEmpresaById(params.idEmpresa);
|
||||
|
||||
setCurrentParams({...params, nombreEmpresa: empData?.nombre});
|
||||
setReportData(null);
|
||||
|
||||
// Resetear totales
|
||||
setTotalesCanillas(initialTotals);
|
||||
setTotalesAccionistas(initialTotals);
|
||||
setTotalesCanillasOtraFecha(initialTotals);
|
||||
setTotalesAccionistasOtraFecha(initialTotals);
|
||||
|
||||
try {
|
||||
const data = await reportesService.getReporteDistribucionCanillas(params);
|
||||
setReportData(data);
|
||||
const noData = (!data.canillas || data.canillas.length === 0) &&
|
||||
(!data.canillasAccionistas || data.canillasAccionistas.length === 0) &&
|
||||
(!data.canillasTodos || data.canillasTodos.length === 0) &&
|
||||
(!data.controlDevolucionesDetalle || data.controlDevolucionesDetalle.length === 0);
|
||||
if (noData) {
|
||||
|
||||
const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
|
||||
(arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || item.remito || item.devueltos || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
|
||||
|
||||
const processedData = {
|
||||
canillas: addIds(data.canillas, 'can'),
|
||||
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
|
||||
canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection
|
||||
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
|
||||
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
|
||||
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
|
||||
controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'),
|
||||
controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo')
|
||||
};
|
||||
setReportData(processedData);
|
||||
|
||||
calculateAndSetTotals(processedData.canillas, setTotalesCanillas);
|
||||
calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas);
|
||||
calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha);
|
||||
calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha);
|
||||
|
||||
const noDataFound = Object.values(processedData).every(arr => !arr || arr.length === 0);
|
||||
if (noDataFound) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el reporte.';
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
setReportData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -80,13 +186,18 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
if (data && data.length > 0) {
|
||||
const exportedData = data.map(item => {
|
||||
const row: Record<string, any> = {};
|
||||
// Excluir el 'id' generado para DataGrid si existe
|
||||
const { id, ...itemData } = item;
|
||||
Object.keys(fields).forEach(key => {
|
||||
row[fields[key]] = item[key];
|
||||
if (key === 'fecha' && item[key]) {
|
||||
row[fields[key]] = new Date(item[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
|
||||
row[fields[key]] = (itemData as any)[key]; // Usar itemData
|
||||
if (key === 'fecha' && (itemData as any)[key]) {
|
||||
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
|
||||
}
|
||||
if ((key === 'totalRendir') && item[key] != null) {
|
||||
row[fields[key]] = parseFloat(item[key]).toFixed(2);
|
||||
if ((key === 'totalRendir') && (itemData as any)[key] != null) {
|
||||
row[fields[key]] = parseFloat((itemData as any)[key]).toFixed(2);
|
||||
}
|
||||
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
|
||||
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
@@ -102,17 +213,19 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
formatAndSheet(reportData.canillas, "Canillitas_Dia", { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
|
||||
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
|
||||
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
|
||||
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
|
||||
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", totalRendir: "A Rendir" });
|
||||
formatAndSheet(reportData.controlDevolucionesDetalle, "CtrlDev_Detalle", { ingresados: "Ingresados", sobrantes: "Sobrantes", sinCargo: "Sin Cargo", publicacion: "Publicación", llevados: "Llevados", devueltos: "Devueltos", tipo: "Tipo" });
|
||||
formatAndSheet(reportData.controlDevolucionesRemitos, "CtrlDev_Remitos", { remito: "Remito Ingresado" });
|
||||
formatAndSheet(reportData.controlDevolucionesOtrosDias, "CtrlDev_OtrosDias", { devueltos: "Devueltos Otros Días" });
|
||||
// Definición de campos para la exportación
|
||||
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
|
||||
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista);
|
||||
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista);
|
||||
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos);
|
||||
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
|
||||
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
|
||||
|
||||
|
||||
let fileName = "ReporteDistribucionCanillitas";
|
||||
let fileName = "ReporteDetalleDistribucionCanillitas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fecha}`;
|
||||
@@ -128,11 +241,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
}
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
setPdfSoloTotales(soloTotales); // Guardar la opción para el nombre del archivo
|
||||
setPdfSoloTotales(soloTotales);
|
||||
try {
|
||||
const blob = await reportesService.getReporteDistribucionCanillasPdf({
|
||||
...currentParams,
|
||||
soloTotales // Pasar el parámetro al servicio
|
||||
soloTotales
|
||||
});
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
@@ -150,45 +263,85 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const renderTableData = (data: DetalleDistribucionCanillaDto[] | DetalleDistribucionCanillaAllDto[], title: string, isAllDto: boolean = false) => {
|
||||
if (!data || data.length === 0) return <Typography sx={{mt:1, fontStyle:'italic'}}>No hay datos para {title.toLowerCase()}.</Typography>;
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2, fontWeight:'bold' }}>{title}</Typography>
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 2 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Publicación</TableCell>
|
||||
<TableCell>{isAllDto ? 'Tipo Vendedor' : 'Canillita'}</TableCell>
|
||||
{ (data[0] as DetalleDistribucionCanillaDto).fecha && <TableCell>Fecha Mov.</TableCell> }
|
||||
<TableCell align="right">Llevados</TableCell>
|
||||
<TableCell align="right">Devueltos</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
<TableCell align="right">A Rendir</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row, idx) => {
|
||||
const item = row as any; // Para acceso dinámico
|
||||
const fechaMov = item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : null;
|
||||
return (
|
||||
<TableRow key={`${title.replace(/\s+/g, '')}-${idx}`}>
|
||||
<TableCell>{item.publicacion}</TableCell>
|
||||
<TableCell>{isAllDto ? item.tipoVendedor : item.canilla}</TableCell>
|
||||
{ fechaMov && <TableCell>{fechaMov}</TableCell> }
|
||||
<TableCell align="right">{item.totalCantSalida.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{item.totalCantEntrada.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{(item.totalCantSalida - item.totalCantEntrada).toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{item.totalRendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
</TableRow>
|
||||
)})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
// --- Definiciones de Columnas ---
|
||||
const commonColumns: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const commonColumnsWithFecha: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 },
|
||||
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const columnsTodos: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
|
||||
{ field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
// --- Custom Footers ---
|
||||
const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno
|
||||
const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => {
|
||||
if (!colConfig) return { width: 100, textAlign: 'right' as const, pr: isPlaceholder ? 0 : 1, fontWeight: 'bold' };
|
||||
const defaultWidth = colConfig.field === 'publicacion' ? 200 : (colConfig.field === 'canilla' || colConfig.field === 'tipoVendedor' ? 150 : 100);
|
||||
return {
|
||||
width: colConfig.width || defaultWidth,
|
||||
flex: colConfig.flex || undefined,
|
||||
minWidth: colConfig.minWidth || colConfig.width || defaultWidth,
|
||||
textAlign: (colConfig.align || 'right') as 'right' | 'left' | 'center',
|
||||
pr: isPlaceholder || colConfig.field === columnsDef[columnsDef.length-1].field ? 0 : 1,
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props
|
||||
<GridFooterContainer {...props} sx={{ // Pasar props y combinar sx
|
||||
...(props.sx as any), // Castear props.sx temporalmente si es necesario
|
||||
justifyContent: 'space-between', alignItems: 'center', width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<GridFooter sx={{ borderTop: 'none', '& .MuiTablePagination-root, & .MuiDataGrid-selectedRowCount': { display: 'none' }}} />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
p: theme => theme.spacing(0, 1), display: 'flex', alignItems: 'center',
|
||||
fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto',
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'publicacion' || c.field === columnsDef[0].field)), textAlign:'right' }}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'canilla' || c.field === 'tipoVendedor' || c.field === columnsDef[1].field), true) }}></Typography>
|
||||
{columnsDef.some(c => c.field === 'fecha') &&
|
||||
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'fecha'), true) }}></Typography>
|
||||
}
|
||||
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'totalCantSalida'))}>{numberFormatter(totals.totalCantSalida)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'totalCantEntrada'))}>{numberFormatter(totals.totalCantEntrada)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'vendidos'))}>{numberFormatter(totals.vendidos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{...getCellStyle(columnsDef.find(c => c.field === 'totalRendir')), pr:0 }}>{currencyFormatter(totals.totalRendir)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
return FooterComponent;
|
||||
};
|
||||
|
||||
const FooterCanillas = useMemo(() => createCustomFooterComponent(totalesCanillas, commonColumns), [totalesCanillas]);
|
||||
const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]);
|
||||
const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
|
||||
const FooterAccionistasOtraFecha = useMemo(() => createCustomFooterComponent(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]);
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
@@ -196,7 +349,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteDetalleDistribucionCanillas
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
onCancel={handleVolverAParametros}
|
||||
onCancel={handleVolverAParametros} // Aunque el componente no lo use directamente.
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
@@ -208,7 +361,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Detalle Distribución Canillitas</Typography>
|
||||
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
|
||||
@@ -225,22 +378,27 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData && (
|
||||
<>
|
||||
{renderTableData(reportData.canillas, "Canillitas")}
|
||||
{renderTableData(reportData.canillasAccionistas, "Accionistas")}
|
||||
{renderTableData(reportData.canillasTodos, "Resumen por Tipo de Vendedor", true)}
|
||||
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
|
||||
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
|
||||
|
||||
<DataGridSection title="Resumen por Tipo de Vendedor" data={reportData.canillasTodos || []} columns={columnsTodos} height={220}/>
|
||||
|
||||
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
|
||||
renderTableData(reportData.canillasLiquidadasOtraFecha, "Canillitas (Liquidados de Otras Fechas)")}
|
||||
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
|
||||
|
||||
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
|
||||
renderTableData(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas (Liquidados de Otras Fechas)")}
|
||||
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && (!reportData || ((!reportData.canillas || reportData.canillas.length === 0) && (!reportData.canillasAccionistas || reportData.canillasAccionistas.length === 0))) &&
|
||||
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
{!loading && !error && reportData &&
|
||||
Object.values(reportData).every(arr => !arr || arr.length === 0) &&
|
||||
<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
// src/pages/Reportes/ReporteListadoDistribucionCanillasImportePage.tsx
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistribucionCanillasImporteDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasImporteDto';
|
||||
import SeleccionaReporteListadoDistribucionCanillasImporte from './SeleccionaReporteListadoDistribucionCanillasImporte';
|
||||
@@ -37,11 +39,13 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
|
||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||
const pubData = await pubService.getPublicacionById(params.idPublicacion);
|
||||
|
||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre});
|
||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre}); // Acceder a publicacion.nombre
|
||||
try {
|
||||
const data = await reportesService.getListadoDistribucionCanillasImporte(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
// DataGrid necesita un 'id' único por fila. La fecha puede no ser suficiente si hay múltiples registros por fecha (aunque el SP agrupa por fecha)
|
||||
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.fecha}-${index}` }));
|
||||
setReportData(dataWithIds);
|
||||
if (dataWithIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -69,22 +73,30 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const dataToExport = reportData.map(item => ({
|
||||
"Fecha": item.fecha, // Ya viene como string dd/MM/yyyy del SP
|
||||
"Llevados": item.llevados,
|
||||
"Devueltos": item.devueltos,
|
||||
"Vendidos": item.vendidos,
|
||||
"Imp. Publicación": item.totalRendirPublicacion,
|
||||
"A Rendir": item.totalRendirGeneral,
|
||||
const dataToExport = reportData.map(({ ...rest }) => ({ // Excluir 'id'
|
||||
"Fecha": rest.fecha,
|
||||
"Llevados": rest.llevados,
|
||||
"Devueltos": rest.devueltos,
|
||||
"Vendidos": rest.vendidos,
|
||||
"Importe Publicación": rest.totalRendirPublicacion,
|
||||
"A Rendir": rest.totalRendirGeneral,
|
||||
}));
|
||||
|
||||
const totales = {
|
||||
"Fecha": "TOTALES",
|
||||
"Llevados": reportData.reduce((sum, r) => sum + r.llevados, 0),
|
||||
"Devueltos": reportData.reduce((sum, r) => sum + r.devueltos, 0),
|
||||
"Vendidos": reportData.reduce((sum, r) => sum + r.vendidos, 0),
|
||||
"Importe Publicación": reportData.reduce((sum, r) => sum + r.totalRendirPublicacion, 0),
|
||||
"A Rendir": reportData.reduce((sum, r) => sum + r.totalRendirGeneral, 0),
|
||||
};
|
||||
dataToExport.push(totales);
|
||||
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
const headers = Object.keys(dataToExport[0]);
|
||||
const headers = Object.keys(dataToExport[0] || {});
|
||||
ws['!cols'] = headers.map(h => {
|
||||
const maxLen = dataToExport.reduce((prev, row) => {
|
||||
const cell = (row as any)[h]?.toString() ?? '';
|
||||
return Math.max(prev, cell.length);
|
||||
}, h.length);
|
||||
const maxLen = Math.max(...dataToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
ws['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
@@ -126,6 +138,68 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const currencyFormatter = (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
|
||||
const numberFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'fecha', headerName: 'Fecha', width: 120, flex: 0.7 },
|
||||
{ field: 'llevados', headerName: 'Llevados', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: numberFormatter, flex:0.6 },
|
||||
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: numberFormatter, flex:0.6 },
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: numberFormatter, flex:0.6 },
|
||||
{ field: 'totalRendirPublicacion', headerName: 'Importe Publicación', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: currencyFormatter, flex:1 },
|
||||
{ field: 'totalRendirGeneral', headerName: 'A Rendir', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: currencyFormatter, flex:1 },
|
||||
];
|
||||
|
||||
const rows = useMemo(() => reportData, [reportData]);
|
||||
|
||||
const totalLlevados = useMemo(() => reportData.reduce((sum, item) => sum + item.llevados, 0), [reportData]);
|
||||
const totalDevueltos = useMemo(() => reportData.reduce((sum, item) => sum + item.devueltos, 0), [reportData]);
|
||||
const totalVendidos = useMemo(() => reportData.reduce((sum, item) => sum + item.vendidos, 0), [reportData]);
|
||||
const totalImpPub = useMemo(() => reportData.reduce((sum, item) => sum + item.totalRendirPublicacion, 0), [reportData]);
|
||||
const totalARendir = useMemo(() => reportData.reduce((sum, item) => sum + item.totalRendirGeneral, 0), [reportData]);
|
||||
|
||||
|
||||
const CustomFooter = () => (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex', // Usar flexbox directamente en el contenedor principal
|
||||
justifyContent: 'space-between', // Mantiene la parte izquierda a la izquierda y la derecha a la derecha
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px',
|
||||
p: theme => theme.spacing(0, 1), // Padding general para el footer container
|
||||
}}>
|
||||
{/* Parte izquierda (donde irían los controles estándar del footer) */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 /* Evita que este box se encoja si los totales son anchos */ }}>
|
||||
<GridFooter sx={{
|
||||
borderTop: 'none',
|
||||
}} />
|
||||
</Box>
|
||||
|
||||
{/* Parte derecha (totales) */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
// marginLeft: 'auto', // Ya no es necesario si GridFooterContainer es flex y justifyContent: 'space-between'
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'auto', // Scroll DENTRO de este Box si los totales son muy anchos
|
||||
maxWidth: 'calc(100% - 50px)', // Ejemplo: Limitar el ancho máximo para dejar espacio a la izquierda (ajustar el valor 50px según sea necesario)
|
||||
// Esto es una medida adicional si la parte izquierda tuviera contenido visible que no queremos que se solape.
|
||||
// Si la parte izquierda realmente no muestra nada, esto podría no ser necesario.
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', width: columns[0].width, textAlign: 'right', pr: 1 }}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[1].width, width: columns[1].width, textAlign: 'right', pr: 1 }}>{numberFormatter(totalLlevados)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[2].width, width: columns[2].width, textAlign: 'right', pr: 1 }}>{numberFormatter(totalDevueltos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[3].width, width: columns[3].width, textAlign: 'right', pr: 1 }}>{numberFormatter(totalVendidos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[4].width, width: columns[4].width, textAlign: 'right', pr: 1 }}>{currencyFormatter(totalImpPub)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', minWidth: columns[5].width, width: columns[5].width, textAlign: 'right', pr: 0 }}>{currencyFormatter(totalARendir)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -144,7 +218,7 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Distribución Canillitas con Importe</Typography>
|
||||
<Typography variant="h5">Reporte: Distribución Canillitas con Importe ({currentParams?.esAccionista ? "Accionistas" : "Canillitas"})</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
@@ -158,36 +232,32 @@ const ReporteListadoDistribucionCanillasImportePage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell align="right">Llevados</TableCell>
|
||||
<TableCell align="right">Devueltos</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
<TableCell align="right">Importe Publicación</TableCell>
|
||||
<TableCell align="right">A Rendir</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.map((row, idx) => (
|
||||
<TableRow key={`importe-${idx}`}>
|
||||
<TableCell>{row.fecha}</TableCell>
|
||||
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.vendidos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.totalRendirPublicacion.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
<TableCell align="right">{row.totalRendirGeneral.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Paper sx={{
|
||||
height: 'calc(100vh - 280px)', // Ajusta esta altura para el footer
|
||||
width: '100%',
|
||||
mt: 2,
|
||||
'& .MuiDataGrid-footerContainer': {
|
||||
minHeight: '52px', // O el alto que necesite tu CustomFooter
|
||||
}
|
||||
}}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: CustomFooter }}
|
||||
density="compact"
|
||||
// pageSizeOptions={[10, 25, 50]} // Descomentar si deseas paginación
|
||||
// initialState={{
|
||||
// pagination: {
|
||||
// paginationModel: { pageSize: 25, page: 0 },
|
||||
// },
|
||||
// }}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
// src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales'; // Importación para localización
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasResponseDto';
|
||||
import type { ListadoDistribucionCanillasSimpleDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasSimpleDto';
|
||||
import type { ListadoDistribucionCanillasPromedioDiaDto } from '../../models/dtos/Reportes/ListadoDistribucionCanillasPromedioDiaDto';
|
||||
// Asegúrate que esta ruta es correcta
|
||||
import SeleccionaReporteListadoDistribucionCanillas from './SeleccionaReporteListadoDistribucionCanillas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaces extendidas para los datos con campos calculados para el DataGrid
|
||||
interface DetalleDiarioCanillasExtendido extends ListadoDistribucionCanillasSimpleDto {
|
||||
id: string;
|
||||
ventaNeta: number;
|
||||
promedio: number; // Promedio acumulativo de Venta Neta
|
||||
porcentajeDevolucion: number;
|
||||
}
|
||||
|
||||
interface PromedioDiaCanillasExtendido extends ListadoDistribucionCanillasPromedioDiaDto {
|
||||
id: string;
|
||||
porcentajeDevolucion: number;
|
||||
}
|
||||
|
||||
const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ListadoDistribucionCanillasResponseDto | null>(null);
|
||||
const [detalleDiarioCalculado, setDetalleDiarioCalculado] = useState<DetalleDiarioCanillasExtendido[]>([]);
|
||||
const [promediosPorDiaCalculado, setPromediosPorDiaCalculado] = useState<PromedioDiaCanillasExtendido[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -23,6 +43,22 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
nombrePublicacion?: string;
|
||||
} | null>(null);
|
||||
|
||||
const [totalesDetalle, setTotalesDetalle] = useState({
|
||||
llevados: 0,
|
||||
devueltos: 0,
|
||||
ventaNeta: 0,
|
||||
promedioGeneralVentaNeta: 0,
|
||||
porcentajeDevolucionGeneral: 0,
|
||||
});
|
||||
|
||||
const [totalesPromedios, setTotalesPromedios] = useState({
|
||||
cantDias: 0,
|
||||
promLlevados: 0,
|
||||
promDevueltos: 0,
|
||||
promVentas: 0,
|
||||
porcentajeDevolucionGeneral: 0,
|
||||
});
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
idPublicacion: number;
|
||||
fechaDesde: string;
|
||||
@@ -31,24 +67,99 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setReportData(null);
|
||||
setDetalleDiarioCalculado([]);
|
||||
setPromediosPorDiaCalculado([]);
|
||||
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 });
|
||||
setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0});
|
||||
|
||||
|
||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||
const pubData = await pubService.getPublicacionById(params.idPublicacion);
|
||||
|
||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre });
|
||||
setCurrentParams({ ...params, nombrePublicacion: pubData?.nombre }); // Asumiendo que pubData es la tupla
|
||||
|
||||
try {
|
||||
const data = await reportesService.getListadoDistribucionCanillas(params);
|
||||
setReportData(data);
|
||||
if ((!data.detalleSimple || data.detalleSimple.length === 0) && (!data.promediosPorDia || data.promediosPorDia.length === 0)) {
|
||||
|
||||
let acumuladoVentaNeta = 0;
|
||||
let diasConActividadDetalle = 0;
|
||||
let ultimoPromedioDetalle = 0;
|
||||
|
||||
const detalleCalculadoLocal = data.detalleSimple.map((item, index) => {
|
||||
const llevados = item.llevados || 0;
|
||||
const devueltos = item.devueltos || 0;
|
||||
const ventaNeta = llevados - devueltos;
|
||||
|
||||
if (llevados > 0) {
|
||||
diasConActividadDetalle++;
|
||||
acumuladoVentaNeta += ventaNeta;
|
||||
}
|
||||
const promedioActual = diasConActividadDetalle > 0 ? acumuladoVentaNeta / diasConActividadDetalle : 0;
|
||||
ultimoPromedioDetalle = promedioActual;
|
||||
|
||||
return {
|
||||
...item,
|
||||
id: `simple-can-${index}`, // o simple-dist-${index}
|
||||
ventaNeta: ventaNeta,
|
||||
promedio: promedioActual,
|
||||
porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0, // Esto es % Devolución real
|
||||
};
|
||||
});
|
||||
setDetalleDiarioCalculado(detalleCalculadoLocal);
|
||||
|
||||
const totalLlevadosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.llevados || 0), 0);
|
||||
const totalDevueltosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.devueltos || 0), 0);
|
||||
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
|
||||
|
||||
setTotalesDetalle({
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: ultimoPromedioDetalle,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
});
|
||||
|
||||
|
||||
// --- Cálculos para promedios y sus totales ---
|
||||
const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => {
|
||||
const promLlevados = item.promedio_Llevados || 0;
|
||||
const promVentas = item.promedio_Ventas || 0;
|
||||
return {
|
||||
...item,
|
||||
id: `prom-can-${index}`, // o prom-dist-${index}
|
||||
// LA COLUMNA EN EL PDF SE LLAMA "% Devolución" PERO PARECE SER "% VENTA"
|
||||
porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
|
||||
porcentajeDevolucion: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
|
||||
};
|
||||
});
|
||||
setPromediosPorDiaCalculado(promediosCalculadoLocal);
|
||||
|
||||
const totalDiasProm = promediosCalculadoLocal.reduce((sum, item) => sum + (item.cant || 0), 0);
|
||||
const totalPonderadoLlevados = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
|
||||
// const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0); // No se usa para el % del PDF
|
||||
const totalPonderadoVentas = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
|
||||
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasProm,
|
||||
promLlevados: totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0,
|
||||
promDevueltos: totalDiasProm > 0 ? promediosCalculadoLocal.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0) / promediosCalculadoLocal.length :0, // Promedio simple para mostrar
|
||||
promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0,
|
||||
// Para la fila "General" de promedios, el PDF usa (Total Prom. Ventas / Total Prom. Llevados) * 100
|
||||
// Usaremos los promedios generales calculados aquí
|
||||
porcentajeDevolucionGeneral: (totalDiasProm > 0 && (totalPonderadoLlevados / totalDiasProm) > 0)
|
||||
? ((totalPonderadoVentas / totalDiasProm) / (totalPonderadoLlevados / totalDiasProm)) * 100
|
||||
: 0,
|
||||
});
|
||||
|
||||
setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal });
|
||||
|
||||
if (detalleCalculadoLocal.length === 0 && promediosCalculadoLocal.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
setReportData(null);
|
||||
// ... (manejo de errores)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -60,46 +171,61 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
setDetalleDiarioCalculado([]);
|
||||
setPromediosPorDiaCalculado([]);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!reportData || (!reportData.detalleSimple?.length && !reportData.promediosPorDia?.length)) {
|
||||
if (!detalleDiarioCalculado.length && !promediosPorDiaCalculado.length) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
if (reportData.detalleSimple?.length) {
|
||||
const simpleToExport = reportData.detalleSimple.map(item => ({
|
||||
"Día": item.dia,
|
||||
"Llevados": item.llevados,
|
||||
"Devueltos": item.devueltos,
|
||||
"Vendidos": item.llevados - item.devueltos,
|
||||
if (detalleDiarioCalculado.length) {
|
||||
const simpleToExport = detalleDiarioCalculado.map(({ id, ...rest }) => ({
|
||||
"Día": rest.dia,
|
||||
"Llevados": rest.llevados,
|
||||
"Devueltos": rest.devueltos,
|
||||
"Venta Neta": rest.ventaNeta,
|
||||
"Promedio": rest.promedio,
|
||||
"% Devolución": rest.porcentajeDevolucion,
|
||||
}));
|
||||
simpleToExport.push({
|
||||
"Día": 0, "Llevados": totalesDetalle.llevados, "Devueltos": totalesDetalle.devueltos,
|
||||
"Venta Neta": totalesDetalle.ventaNeta, "Promedio": totalesDetalle.promedioGeneralVentaNeta,
|
||||
"% Devolución": totalesDetalle.porcentajeDevolucionGeneral,
|
||||
});
|
||||
const wsSimple = XLSX.utils.json_to_sheet(simpleToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsSimple, "DetalleDiario");
|
||||
XLSX.utils.book_append_sheet(wb, wsSimple, "DetalleDiarioCanillitas");
|
||||
}
|
||||
|
||||
if (reportData.promediosPorDia?.length) {
|
||||
const promediosToExport = reportData.promediosPorDia.map(item => ({
|
||||
"Día Semana": item.dia,
|
||||
"Cant. Días": item.cant,
|
||||
"Prom. Llevados": item.promedio_Llevados,
|
||||
"Prom. Devueltos": item.promedio_Devueltos,
|
||||
"Prom. Vendidos": item.promedio_Ventas,
|
||||
if (promediosPorDiaCalculado.length) {
|
||||
const promediosToExport = promediosPorDiaCalculado.map(({ id, ...rest }) => ({
|
||||
"Día Semana": rest.dia,
|
||||
"Cant. Días": rest.cant,
|
||||
"Prom. Llevados": rest.promedio_Llevados,
|
||||
"Prom. Devueltos": rest.promedio_Devueltos,
|
||||
"Prom. Ventas": rest.promedio_Ventas,
|
||||
"% Devolución": rest.porcentajeDevolucion,
|
||||
}));
|
||||
promediosToExport.push({
|
||||
"Día Semana": "General", "Cant. Días": totalesPromedios.cantDias,
|
||||
"Prom. Llevados": totalesPromedios.promLlevados, "Prom. Devueltos": totalesPromedios.promDevueltos,
|
||||
"Prom. Ventas": totalesPromedios.promVentas, "% Devolución": totalesPromedios.porcentajeDevolucionGeneral
|
||||
});
|
||||
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDiaSemana");
|
||||
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosCanillitas");
|
||||
}
|
||||
|
||||
let fileName = "ListadoDistribucionCanillas";
|
||||
let fileName = "ListadoDistribucionCanillitas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [reportData, currentParams]);
|
||||
}, [detalleDiarioCalculado, promediosPorDiaCalculado, currentParams, totalesDetalle, totalesPromedios]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
if (!currentParams) {
|
||||
@@ -126,6 +252,62 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const columnsDetalle: GridColDef[] = [
|
||||
{ field: 'dia', headerName: 'Día', type: 'number', width: 70, align: 'right', headerAlign: 'right', sortable: false },
|
||||
{ field: 'llevados', headerName: 'Llevados', type: 'number', flex: 0.7, minWidth: 90, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', flex: 0.7, minWidth: 90, align: 'right', headerAlign: 'right', valueFormatter: (value) => value != null ? Number(value).toLocaleString('es-AR') : '0' },
|
||||
{ field: 'ventaNeta', headerName: 'Venta Neta', type: 'number', flex: 0.8, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||
{ field: 'promedio', headerName: 'Promedio', type: 'number', flex: 0.8, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||
{ field: 'porcentajeDevolucion', headerName: '% Devolución', type: 'number', flex: 0.8, minWidth: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => `${Number(value).toLocaleString('es-AR', { maximumFractionDigits: 2 })}%` },
|
||||
];
|
||||
|
||||
const columnsPromedios: GridColDef[] = [
|
||||
{ field: 'dia', headerName: 'Día Semana', width: 140, flex: 1 },
|
||||
{ field: 'cant', headerName: 'Cant. Días', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||
{ field: 'promedio_Llevados', headerName: 'Prom. Llevados', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||
{ field: 'promedio_Devueltos', headerName: 'Prom. Devueltos', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||
{ field: 'promedio_Ventas', headerName: 'Prom. Ventas', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||
{ field: 'porcentajeDevolucion', headerName: '% Devolución', type: 'number', flex: 0.8, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => `${Number(value).toLocaleString('es-AR', { maximumFractionDigits: 2 })}%` },
|
||||
];
|
||||
|
||||
|
||||
|
||||
// --- Custom Footer para Detalle Diario ---
|
||||
const CustomFooterDetalle = () => (
|
||||
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
{/* Contenedor para los elementos del footer por defecto (paginación, etc.) */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}> {/* Permitir que se encoja un poco si es necesario */}
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
{/* Contenedor para tus totales */}
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
// --- Custom Footer para Promedios por Día ---
|
||||
const CustomFooterPromedios = () => (
|
||||
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -157,69 +339,54 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Publicación: {currentParams?.nombrePublicacion} |
|
||||
Fechas: {currentParams?.fechaDesde} al {currentParams?.fechaHasta}
|
||||
</Typography>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Detalle Diario</Typography>
|
||||
{reportData.detalleSimple && reportData.detalleSimple.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Día</TableCell>
|
||||
<TableCell align="right">Llevados</TableCell>
|
||||
<TableCell align="right">Devueltos</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.detalleSimple.map((row, idx) => (
|
||||
<TableRow key={`simple-${idx}`}>
|
||||
<TableCell>{row.dia}</TableCell>
|
||||
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{(row.llevados - row.devueltos).toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : (<Typography>No hay datos de detalle diario.</Typography>)}
|
||||
{detalleDiarioCalculado.length > 0 ? (
|
||||
<Paper sx={{ height: 450, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={detalleDiarioCalculado} // Usar los datos calculados
|
||||
columns={columnsDetalle}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterDetalle }}
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de detalle diario.</Typography>)}
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
|
||||
{reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Día Semana</TableCell>
|
||||
<TableCell align="right">Cant. Días</TableCell>
|
||||
<TableCell align="right">Prom. Llevados</TableCell>
|
||||
<TableCell align="right">Prom. Devueltos</TableCell>
|
||||
<TableCell align="right">Prom. Ventas</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.promediosPorDia.map((row, idx) => (
|
||||
<TableRow key={`promedio-${idx}`}>
|
||||
<TableCell>{row.dia}</TableCell>
|
||||
<TableCell align="right">{row.cant.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.promedio_Llevados.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.promedio_Devueltos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.promedio_Ventas.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : (<Typography>No hay datos de promedios por día.</Typography>)}
|
||||
{promediosPorDiaCalculado.length > 0 ? (
|
||||
<Paper sx={{ height: 360, width: '100%', '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={promediosPorDiaCalculado} // Usar los datos calculados
|
||||
columns={columnsPromedios}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterPromedios }}
|
||||
hideFooterSelectedRowCount
|
||||
sx={{
|
||||
'& .MuiTablePagination-root': { // Oculta el paginador por defecto
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de promedios por día.</Typography>)}
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && (!reportData || ((!reportData.detalleSimple || reportData.detalleSimple.length === 0) && (!reportData.promediosPorDia || reportData.promediosPorDia.length === 0))) &&
|
||||
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteListadoDistribucionCanillasPage;
|
||||
export default ReporteListadoDistribucionCanillasPage; // Asegúrate de que el nombre del archivo coincida con este export
|
||||
@@ -1,50 +1,116 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
// src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
|
||||
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral';
|
||||
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
|
||||
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
|
||||
interface TotalesComunes {
|
||||
totalCantSalida: number;
|
||||
totalCantEntrada: number;
|
||||
vendidos: number;
|
||||
totalRendir: number;
|
||||
}
|
||||
|
||||
const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
idPublicacion: number;
|
||||
fechaDesde: string; // Primer día del mes
|
||||
fechaHasta: string; // Último día del mes
|
||||
nombrePublicacion?: string; // Para el nombre del archivo
|
||||
mesAnioParaNombreArchivo?: string; // Para el nombre del archivo (ej. YYYY-MM)
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
|
||||
|
||||
// Estados para los totales de cada sección
|
||||
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
|
||||
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesAccionistas, setTotalesAccionistas] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesTodos, setTotalesTodos] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState<TotalesComunes>(initialTotals);
|
||||
const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState<TotalesComunes>(initialTotals);
|
||||
|
||||
// --- Formateadores ---
|
||||
const currencyFormatter = (value: number | null | undefined) =>
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
|
||||
const numberFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const calculateAndSetTotals = (dataArray: Array<any> | undefined, setTotalsFunc: React.Dispatch<React.SetStateAction<TotalesComunes>>) => {
|
||||
if (dataArray && dataArray.length > 0) {
|
||||
const totals = dataArray.reduce((acc, item) => {
|
||||
acc.totalCantSalida += Number(item.totalCantSalida) || 0;
|
||||
acc.totalCantEntrada += Number(item.totalCantEntrada) || 0;
|
||||
acc.totalRendir += Number(item.totalRendir) || 0;
|
||||
return acc;
|
||||
}, { totalCantSalida: 0, totalCantEntrada: 0, totalRendir: 0 });
|
||||
totals.vendidos = totals.totalCantSalida - totals.totalCantEntrada;
|
||||
setTotalsFunc(totals);
|
||||
} else {
|
||||
setTotalsFunc(initialTotals);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
idPublicacion: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
|
||||
// Para el nombre del archivo y título del PDF
|
||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||
const pubData = await pubService.getPublicacionById(params.idPublicacion);
|
||||
const mesAnioParts = params.fechaDesde.split('-'); // YYYY-MM-DD -> [YYYY, MM, DD]
|
||||
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
|
||||
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empresaService.getEmpresaById(params.idEmpresa);
|
||||
|
||||
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
|
||||
setReportData(null); // Limpiar datos antiguos
|
||||
|
||||
// Resetear totales
|
||||
setTotalesCanillas(initialTotals);
|
||||
setTotalesAccionistas(initialTotals);
|
||||
setTotalesTodos(initialTotals);
|
||||
setTotalesCanillasOtraFecha(initialTotals);
|
||||
setTotalesAccionistasOtraFecha(initialTotals);
|
||||
|
||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
|
||||
try {
|
||||
const data = await reportesService.getListadoDistribucionGeneral(params);
|
||||
setReportData(data);
|
||||
if ((!data.resumen || data.resumen.length === 0) && (!data.promediosPorDia || data.promediosPorDia.length === 0)) {
|
||||
const data = await reportesService.getReporteDistribucionCanillas(params);
|
||||
|
||||
const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
|
||||
(arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
|
||||
|
||||
const processedData = {
|
||||
canillas: addIds(data.canillas, 'can'),
|
||||
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
|
||||
canillasTodos: addIds(data.canillasTodos, 'all'),
|
||||
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
|
||||
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
|
||||
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
|
||||
controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'),
|
||||
controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo')
|
||||
};
|
||||
setReportData(processedData);
|
||||
|
||||
// Calcular y setear totales para cada sección
|
||||
calculateAndSetTotals(processedData.canillas, setTotalesCanillas);
|
||||
calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas);
|
||||
calculateAndSetTotals(processedData.canillasTodos, setTotalesTodos);
|
||||
calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha);
|
||||
calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha);
|
||||
|
||||
const noData = (!data.canillas || data.canillas.length === 0) &&
|
||||
(!data.canillasAccionistas || data.canillasAccionistas.length === 0) &&
|
||||
(!data.canillasTodos || data.canillasTodos.length === 0); // Podrías añadir más chequeos si es necesario
|
||||
if (noData) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -68,62 +134,96 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) {
|
||||
if (!reportData) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
if (reportData.resumen?.length) {
|
||||
const resumenToExport = reportData.resumen.map(item => ({
|
||||
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
|
||||
"Tirada": item.cantidadTirada,
|
||||
"Sin Cargo": item.sinCargo,
|
||||
"Perdidos": item.perdidos,
|
||||
"Llevados": item.llevados,
|
||||
"Devueltos": item.devueltos,
|
||||
"Vendidos": item.vendidos,
|
||||
}));
|
||||
const wsResumen = XLSX.utils.json_to_sheet(resumenToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario");
|
||||
const formatAndSheet = (
|
||||
data: any[],
|
||||
sheetName: string,
|
||||
fields: Record<string, string>,
|
||||
totals?: TotalesComunes
|
||||
) => {
|
||||
if (data && data.length > 0) {
|
||||
let exportedData = data.map(item => {
|
||||
const row: Record<string, any> = {};
|
||||
const { id, ...itemData } = item; // Excluir el 'id' generado
|
||||
Object.keys(fields).forEach(key => {
|
||||
row[fields[key]] = (itemData as any)[key];
|
||||
if (key === 'fecha' && (itemData as any)[key]) {
|
||||
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
|
||||
}
|
||||
if ((key === 'totalRendir') && (itemData as any)[key] != null) {
|
||||
row[fields[key]] = parseFloat((itemData as any)[key]); // Mantener como número para suma en Excel
|
||||
}
|
||||
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
|
||||
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
|
||||
}
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
if (totals) {
|
||||
const totalRow: Record<string, any> = {};
|
||||
const fieldKeys = Object.keys(fields);
|
||||
totalRow[fields[fieldKeys[0]]] = "TOTALES"; // Título en la primera columna
|
||||
if (fields.totalCantSalida) totalRow[fields.totalCantSalida] = totals.totalCantSalida;
|
||||
if (fields.totalCantEntrada) totalRow[fields.totalCantEntrada] = totals.totalCantEntrada;
|
||||
if (fields.vendidos) totalRow[fields.vendidos] = totals.vendidos;
|
||||
if (fields.totalRendir) totalRow[fields.totalRendir] = totals.totalRendir;
|
||||
exportedData.push(totalRow);
|
||||
}
|
||||
|
||||
if (reportData.promediosPorDia?.length) {
|
||||
const promediosToExport = reportData.promediosPorDia.map(item => ({
|
||||
"Día Semana": item.dia,
|
||||
"Cant. Días": item.cantidadDias,
|
||||
"Prom. Tirada": item.promedioTirada,
|
||||
"Prom. Sin Cargo": item.promedioSinCargo,
|
||||
"Prom. Perdidos": item.promedioPerdidos,
|
||||
"Prom. Llevados": item.promedioLlevados,
|
||||
"Prom. Devueltos": item.promedioDevueltos,
|
||||
"Prom. Vendidos": item.promedioVendidos,
|
||||
}));
|
||||
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
|
||||
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
let fileName = "ListadoDistribucionGeneral";
|
||||
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsCtrlDevDetalle = { ingresados: "Ingresados", sobrantes: "Sobrantes", sinCargo: "Sin Cargo", publicacion: "Publicación", llevados: "Llevados", devueltos: "Devueltos", tipo: "Tipo" };
|
||||
const fieldsCtrlDevRemitos = { remito: "Remito Ingresado" };
|
||||
const fieldsCtrlDevOtrosDias = { devueltos: "Devueltos Otros Días" };
|
||||
|
||||
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista, totalesCanillas);
|
||||
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista, totalesAccionistas);
|
||||
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos, totalesTodos);
|
||||
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq, totalesCanillasOtraFecha);
|
||||
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq, totalesAccionistasOtraFecha);
|
||||
formatAndSheet(reportData.controlDevolucionesDetalle, "CtrlDev_Detalle", fieldsCtrlDevDetalle); // Sin totales para estos
|
||||
formatAndSheet(reportData.controlDevolucionesRemitos, "CtrlDev_Remitos", fieldsCtrlDevRemitos);
|
||||
formatAndSheet(reportData.controlDevolucionesOtrosDias, "CtrlDev_OtrosDias", fieldsCtrlDevOtrosDias);
|
||||
|
||||
let fileName = "ReporteDetalleDistribucionCanillitas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
|
||||
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fecha}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [reportData, currentParams]);
|
||||
}, [reportData, currentParams, totalesCanillas, totalesAccionistas, totalesTodos, totalesCanillasOtraFecha, totalesAccionistasOtraFecha]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
const handleGenerarYAbrirPdf = useCallback(async (soloTotales: boolean) => {
|
||||
if (!currentParams) {
|
||||
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
|
||||
return;
|
||||
}
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
setPdfSoloTotales(soloTotales);
|
||||
try {
|
||||
const blob = await reportesService.getListadoDistribucionGeneralPdf({
|
||||
idPublicacion: currentParams.idPublicacion,
|
||||
fechaDesde: currentParams.fechaDesde, // El servicio y SP esperan fechaDesde para el mes/año
|
||||
fechaHasta: currentParams.fechaHasta // El SP no usa esta, pero el servicio de reporte sí para el nombre
|
||||
const blob = await reportesService.getReporteDistribucionCanillasPdf({
|
||||
...currentParams,
|
||||
soloTotales
|
||||
});
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
@@ -141,13 +241,99 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
// Definiciones de columnas
|
||||
const commonColumns: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const commonColumnsWithFecha: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 },
|
||||
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const columnsTodos: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
|
||||
{ field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
|
||||
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const columnsCtrlDevDetalle: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.5 },
|
||||
{ field: 'tipo', headerName: 'Tipo', width: 100, flex: 0.8 },
|
||||
{ field: 'ingresados', headerName: 'Ingresados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
|
||||
{ field: 'sobrantes', headerName: 'Sobrantes', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
|
||||
{ field: 'sinCargo', headerName: 'Sin Cargo', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
|
||||
{ field: 'llevados', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
|
||||
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
|
||||
];
|
||||
|
||||
const columnsCtrlDevRemitos: GridColDef[] = [
|
||||
{ field: 'remito', headerName: 'Remito Ingresado', flex: 1 },
|
||||
];
|
||||
|
||||
const columnsCtrlDevOtrosDias: GridColDef[] = [
|
||||
{ field: 'devueltos', headerName: 'Devueltos Otros Días', flex: 1 },
|
||||
];
|
||||
|
||||
// Memoizar filas (los IDs ya se añaden en handleGenerarReporte)
|
||||
const rowsCanillas = useMemo(() => reportData?.canillas ?? [], [reportData]);
|
||||
const rowsAccionistas = useMemo(() => reportData?.canillasAccionistas ?? [], [reportData]);
|
||||
const rowsTodos = useMemo(() => reportData?.canillasTodos ?? [], [reportData]);
|
||||
const rowsCanillasOtraFecha = useMemo(() => reportData?.canillasLiquidadasOtraFecha ?? [], [reportData]);
|
||||
const rowsAccionistasOtraFecha = useMemo(() => reportData?.canillasAccionistasLiquidadasOtraFecha ?? [], [reportData]);
|
||||
const rowsCtrlDevDetalle = useMemo(() => reportData?.controlDevolucionesDetalle ?? [], [reportData]);
|
||||
const rowsCtrlDevRemitos = useMemo(() => reportData?.controlDevolucionesRemitos ?? [], [reportData]);
|
||||
const rowsCtrlDevOtrosDias = useMemo(() => reportData?.controlDevolucionesOtrosDias ?? [], [reportData]);
|
||||
|
||||
// --- Custom Footers ---
|
||||
// eslint-disable-next-line react/display-name
|
||||
const createCustomFooter = (totals: TotalesComunes, columns: GridColDef[]) => () => (
|
||||
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, minWidth: '300px' }}>
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ flex: columns[0].flex, width: columns[0].width, textAlign: 'right', fontWeight: 'bold' }}>TOTALES:</Typography>
|
||||
{columns[1].field !== 'tipoVendedor' && <Typography variant="subtitle2" sx={{ flex: columns[1].flex, width: columns[1].width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Canilla/Tipo */ }
|
||||
{columns[1].field === 'tipoVendedor' && <Typography variant="subtitle2" sx={{ flex: columns[1].flex, width: columns[1].width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Canilla/Tipo */ }
|
||||
|
||||
{columns.find(c => c.field === 'fecha') && <Typography variant="subtitle2" sx={{ flex: columns.find(c=>c.field === 'fecha')?.flex, width: columns.find(c=>c.field === 'fecha')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Fecha */}
|
||||
|
||||
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalCantSalida')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.totalCantSalida)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalCantEntrada')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.totalCantEntrada)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'vendidos')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.vendidos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalRendir')?.width, textAlign: 'right', fontWeight: 'bold' }}>{currencyFormatter(totals.totalRendir)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
const CustomFooterCanillas = useMemo(() => createCustomFooter(totalesCanillas, commonColumns), [totalesCanillas]);
|
||||
const CustomFooterAccionistas = useMemo(() => createCustomFooter(totalesAccionistas, commonColumns), [totalesAccionistas]);
|
||||
const CustomFooterTodos = useMemo(() => createCustomFooter(totalesTodos, columnsTodos), [totalesTodos]);
|
||||
const CustomFooterCanillasOtraFecha = useMemo(() => createCustomFooter(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
|
||||
const CustomFooterAccionistasOtraFecha = useMemo(() => createCustomFooter(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]);
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteListadoDistribucionGeneral
|
||||
<SeleccionaReporteDetalleDistribucionCanillas
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
onCancel={handleVolverAParametros}
|
||||
onCancel={handleVolverAParametros} // Asumo que no se usa, ya que el selector no tiene botón de cancelar
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
@@ -159,10 +345,13 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Listado Distribución General</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
|
||||
</Button>
|
||||
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
|
||||
Exportar a Excel
|
||||
@@ -173,80 +362,151 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography>
|
||||
{reportData.resumen && reportData.resumen.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell align="right">Tirada</TableCell>
|
||||
<TableCell align="right">Sin Cargo</TableCell>
|
||||
<TableCell align="right">Perdidos</TableCell>
|
||||
<TableCell align="right">Llevados</TableCell>
|
||||
<TableCell align="right">Devueltos</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.resumen.map((row, idx) => (
|
||||
<TableRow key={`resumen-${idx}`}>
|
||||
<TableCell>{row.fecha ? new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
|
||||
<TableCell align="right">{row.cantidadTirada.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.sinCargo.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.perdidos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.vendidos.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : (<Typography>No hay datos de resumen diario.</Typography>)}
|
||||
{/* Canillitas (del día) */}
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (del día)</Typography>
|
||||
{rowsCanillas.length > 0 ? (
|
||||
<Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={rowsCanillas}
|
||||
columns={commonColumns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterCanillas }}
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para canillitas (del día).</Typography>)}
|
||||
|
||||
<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>)}
|
||||
{/* Accionistas (del día) */}
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (del día)</Typography>
|
||||
{rowsAccionistas.length > 0 ? (
|
||||
<Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={rowsAccionistas}
|
||||
columns={commonColumns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterAccionistas }}
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para accionistas (del día).</Typography>)}
|
||||
|
||||
{/* Resumen por Tipo de Vendedor (del día) */}
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen por Tipo de Vendedor (del día)</Typography>
|
||||
{rowsTodos.length > 0 ? (
|
||||
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={rowsTodos}
|
||||
columns={columnsTodos}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterTodos }}
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para resumen por tipo de vendedor (del día).</Typography>)}
|
||||
|
||||
{/* Canillitas (Liquidados de Otras Fechas) */}
|
||||
{rowsCanillasOtraFecha.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (Liquidados de Otras Fechas)</Typography>
|
||||
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={rowsCanillasOtraFecha}
|
||||
columns={commonColumnsWithFecha}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterCanillasOtraFecha }}
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Accionistas (Liquidados de Otras Fechas) */}
|
||||
{rowsAccionistasOtraFecha.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (Liquidados de Otras Fechas)</Typography>
|
||||
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
|
||||
<DataGrid
|
||||
rows={rowsAccionistasOtraFecha}
|
||||
columns={commonColumnsWithFecha}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterAccionistasOtraFecha }}
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Control Devoluciones - Detalle */}
|
||||
{rowsCtrlDevDetalle.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Detalle</Typography>
|
||||
<Paper sx={{ height: 300, width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsCtrlDevDetalle}
|
||||
columns={columnsCtrlDevDetalle}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
hideFooterSelectedRowCount // Sin footer personalizado para estos
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Control Devoluciones - Remitos */}
|
||||
{rowsCtrlDevRemitos.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Remitos Ingresados</Typography>
|
||||
<Paper sx={{ height: 200, width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsCtrlDevRemitos}
|
||||
columns={columnsCtrlDevRemitos}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
autoHeight
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Control Devoluciones - Otros Días */}
|
||||
{rowsCtrlDevOtrosDias.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Otros Días</Typography>
|
||||
<Paper sx={{ height: 200, width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsCtrlDevOtrosDias}
|
||||
columns={columnsCtrlDevOtrosDias}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
autoHeight
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && (!reportData ||
|
||||
(rowsCanillas.length === 0 && rowsAccionistas.length === 0 && rowsTodos.length === 0 &&
|
||||
rowsCanillasOtraFecha.length === 0 && rowsAccionistasOtraFecha.length === 0 &&
|
||||
rowsCtrlDevDetalle.length === 0 && rowsCtrlDevRemitos.length === 0 && rowsCtrlDevOtrosDias.length === 0
|
||||
)) &&
|
||||
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteListadoDistribucionGeneralPage;
|
||||
export default ReporteDetalleDistribucionCanillasPage;
|
||||
421
Frontend/src/pages/Reportes/ReporteListadoDistribucionPage.tsx
Normal file
421
Frontend/src/pages/Reportes/ReporteListadoDistribucionPage.tsx
Normal 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;
|
||||
@@ -1,14 +1,27 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
// Corregir importaciones de DTOs
|
||||
import type { MovimientoBobinasPorEstadoResponseDto } from '../../models/dtos/Reportes/MovimientoBobinasPorEstadoResponseDto';
|
||||
import type { MovimientoBobinaEstadoDetalleDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoDetalleDto';
|
||||
import type { MovimientoBobinaEstadoTotalDto } from '../../models/dtos/Reportes/MovimientoBobinaEstadoTotalDto';
|
||||
|
||||
import SeleccionaReporteMovimientoBobinasEstado from './SeleccionaReporteMovimientoBobinasEstado';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaces extendidas para DataGrid con 'id'
|
||||
interface DetalleMovimientoDataGrid extends MovimientoBobinaEstadoDetalleDto { // Usar el DTO correcto
|
||||
id: string;
|
||||
}
|
||||
interface TotalPorEstadoDataGrid extends MovimientoBobinaEstadoTotalDto { // Usar el DTO correcto
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<MovimientoBobinasPorEstadoResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -20,8 +33,16 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
idPlanta: number;
|
||||
nombrePlanta?: string;
|
||||
} | null>(null);
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const dateLocaleFormatter = (value: string | null | undefined) =>
|
||||
value ? new Date(value).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-';
|
||||
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
@@ -33,8 +54,14 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
setCurrentParams(params);
|
||||
try {
|
||||
const data = await reportesService.getMovimientoBobinasEstado(params);
|
||||
setReportData(data);
|
||||
if ((!data.detalle || data.detalle.length === 0) && (!data.totales || data.totales.length === 0)) {
|
||||
|
||||
const processedData: MovimientoBobinasPorEstadoResponseDto = {
|
||||
detalle: data.detalle?.map((item, index) => ({ ...item, id: `detalle-${index}-${item.numeroRemito}-${item.tipoBobina}` })) || [],
|
||||
totales: data.totales?.map((item, index) => ({ ...item, id: `total-${index}-${item.tipoMovimiento}` })) || []
|
||||
};
|
||||
setReportData(processedData);
|
||||
|
||||
if ((!processedData.detalle || processedData.detalle.length === 0) && (!processedData.totales || processedData.totales.length === 0)) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -65,7 +92,6 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Hoja de Detalles
|
||||
if (reportData.detalle?.length) {
|
||||
const detalleToExport = reportData.detalle.map(item => ({
|
||||
"Tipo Bobina": item.tipoBobina,
|
||||
@@ -76,18 +102,11 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
}));
|
||||
const wsDetalle = XLSX.utils.json_to_sheet(detalleToExport);
|
||||
const headersDetalle = Object.keys(detalleToExport[0] || {});
|
||||
wsDetalle['!cols'] = headersDetalle.map(h => {
|
||||
const maxLen = detalleToExport.reduce((prev, row) => {
|
||||
const cell = (row as any)[h]?.toString() ?? '';
|
||||
return Math.max(prev, cell.length);
|
||||
}, h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
wsDetalle['!cols'] = headersDetalle.map(h => ({ wch: Math.max(...detalleToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
wsDetalle['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
XLSX.utils.book_append_sheet(wb, wsDetalle, "DetalleMovimientos");
|
||||
}
|
||||
|
||||
// Hoja de Totales
|
||||
if (reportData.totales?.length) {
|
||||
const totalesToExport = reportData.totales.map(item => ({
|
||||
"Tipo Movimiento": item.tipoMovimiento,
|
||||
@@ -96,20 +115,15 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
}));
|
||||
const wsTotales = XLSX.utils.json_to_sheet(totalesToExport);
|
||||
const headersTotales = Object.keys(totalesToExport[0] || {});
|
||||
wsTotales['!cols'] = headersTotales.map(h => {
|
||||
const maxLen = totalesToExport.reduce((prev, row) => {
|
||||
const cell = (row as any)[h]?.toString() ?? '';
|
||||
return Math.max(prev, cell.length);
|
||||
}, h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
wsTotales['!cols'] = headersTotales.map(h => ({ wch: Math.max(...totalesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
wsTotales['!freeze'] = { xSplit: 0, ySplit: 1 };
|
||||
XLSX.utils.book_append_sheet(wb, wsTotales, "TotalesPorEstado");
|
||||
}
|
||||
|
||||
let fileName = "ReporteMovimientoBobinasEstado";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_Planta${currentParams.idPlanta}`;
|
||||
fileName += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
@@ -140,6 +154,56 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
// Columnas para DataGrid de Detalle de Movimientos
|
||||
const columnsDetalle: GridColDef<DetalleMovimientoDataGrid>[] = [ // Tipar con la interfaz correcta
|
||||
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 220, flex: 1.5 },
|
||||
{ field: 'numeroRemito', headerName: 'Nro Remito', width: 130, flex: 0.8 },
|
||||
{ field: 'fechaMovimiento', headerName: 'Fecha Movimiento', width: 150, flex: 1, valueFormatter: (value) => dateLocaleFormatter(value as string) },
|
||||
{ field: 'cantidad', headerName: 'Cantidad', type: 'number', width: 120, align: 'right', headerAlign: 'right', flex: 0.7, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'tipoMovimiento', headerName: 'Tipo Movimiento', width: 150, flex: 1 },
|
||||
];
|
||||
|
||||
// Columnas para DataGrid de Totales por Estado
|
||||
const columnsTotales: GridColDef<TotalPorEstadoDataGrid>[] = [ // Tipar con la interfaz correcta
|
||||
{ field: 'tipoMovimiento', headerName: 'Tipo Movimiento', width: 200, flex: 1 },
|
||||
{ field: 'totalBobinas', headerName: 'Total Bobinas', type: 'number', width: 150, align: 'right', headerAlign: 'right', flex: 0.8, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'totalKilos', headerName: 'Total Kilos', type: 'number', width: 150, align: 'right', headerAlign: 'right', flex: 0.8, valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const rowsDetalle = useMemo(() => (reportData?.detalle as DetalleMovimientoDataGrid[]) || [], [reportData]);
|
||||
const rowsTotales = useMemo(() => (reportData?.totales as TotalPorEstadoDataGrid[]) || [], [reportData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooterDetalle = () => (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px',
|
||||
}}>
|
||||
<Box sx={{
|
||||
flexGrow:1,
|
||||
display:'flex',
|
||||
justifyContent:'flex-start',
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none',
|
||||
width: 'auto',
|
||||
'& .MuiToolbar-root': {
|
||||
paddingLeft: (theme) => theme.spacing(1),
|
||||
paddingRight: (theme) => theme.spacing(1),
|
||||
},
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -155,10 +219,11 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Movimiento de Bobinas por Estado</Typography>
|
||||
<Typography variant="h5">Reporte: Movimiento de Bobinas por Estado {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
onClick={handleGenerarYAbrirPdf}
|
||||
@@ -182,74 +247,51 @@ const ReporteMovimientoBobinasEstadoPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData && (
|
||||
<> {/* Usamos un Fragmento React para agrupar los elementos sin añadir un div extra */}
|
||||
{/* Tabla de Detalle de Movimientos */}
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
||||
Detalle de Movimientos
|
||||
</Typography>
|
||||
{reportData.detalle && reportData.detalle.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '400px', mb: 3 }}> {/* Añadido mb: 3 para espaciado */}
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Bobina</TableCell>
|
||||
<TableCell>Nro Remito</TableCell>
|
||||
<TableCell>Fecha Movimiento</TableCell>
|
||||
<TableCell align="right">Cantidad</TableCell>
|
||||
<TableCell>Tipo Movimiento</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.detalle.map((row, idx) => (
|
||||
<TableRow key={`detalle-${idx}`}>
|
||||
<TableCell>{row.tipoBobina}</TableCell>
|
||||
<TableCell>{row.numeroRemito}</TableCell>
|
||||
<TableCell>{row.fechaMovimiento ? new Date(row.fechaMovimiento).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
|
||||
<TableCell align="right">{row.cantidad.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell>{row.tipoMovimiento}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{rowsDetalle.length > 0 ? (
|
||||
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsDetalle}
|
||||
columns={columnsDetalle}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
sx={{ height: 'calc(100vh - 350px)' }}
|
||||
slots={{ footer: CustomFooterDetalle }}
|
||||
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{ mb: 3 }}>No hay detalles de movimientos para mostrar.</Typography>
|
||||
<Typography sx={{ mb: 3, fontStyle: 'italic' }}>No hay detalles de movimientos para mostrar.</Typography>
|
||||
)}
|
||||
|
||||
{/* Tabla de Totales por Estado */}
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>
|
||||
Totales por Estado
|
||||
</Typography>
|
||||
{reportData.totales && reportData.totales.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxWidth: '600px' }}> {/* Limitamos el ancho para tablas pequeñas */}
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Movimiento</TableCell>
|
||||
<TableCell align="right">Total Bobinas</TableCell>
|
||||
<TableCell align="right">Total Kilos</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.totales.map((row, idx) => (
|
||||
<TableRow key={`total-${idx}`}>
|
||||
<TableCell>{row.tipoMovimiento}</TableCell>
|
||||
<TableCell align="right">{row.totalBobinas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.totalKilos.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{rowsTotales.length > 0 ? (
|
||||
<Paper sx={{ width: '100%', maxWidth: '700px' }}>
|
||||
<DataGrid
|
||||
rows={rowsTotales}
|
||||
columns={columnsTotales}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
autoHeight
|
||||
hideFooter
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography>No hay totales para mostrar.</Typography>
|
||||
<Typography sx={{fontStyle: 'italic'}}>No hay totales por estado para mostrar.</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && !reportData && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid'; // Importaciones para DataGrid
|
||||
import { esES } from '@mui/x-data-grid/locales'; // Para localización
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { MovimientoBobinasDto } from '../../models/dtos/Reportes/MovimientoBobinasDto';
|
||||
import SeleccionaReporteMovimientoBobinas from './SeleccionaReporteMovimientoBobinas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Definición de la interfaz extendida para DataGrid (con 'id')
|
||||
interface MovimientoBobinasDataGridDto extends MovimientoBobinasDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<MovimientoBobinasDto[]>([]);
|
||||
const [reportData, setReportData] = useState<MovimientoBobinasDataGridDto[]>([]); // Usar el tipo extendido
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -20,8 +26,12 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
idPlanta: number;
|
||||
nombrePlanta?: string;
|
||||
} | null>(null);
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR') : '';
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
@@ -30,11 +40,20 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
// Opcional: Obtener nombre de la planta
|
||||
// const plantaService = (await import('../../services/Maestros/plantaService')).default;
|
||||
// const plantaData = await plantaService.getPlantaById(params.idPlanta);
|
||||
// setCurrentParams({...params, nombrePlanta: plantaData?.nombre});
|
||||
setCurrentParams(params);
|
||||
try {
|
||||
const data = await reportesService.getMovimientoBobinas(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
// Añadir 'id' único a cada fila para DataGrid
|
||||
const dataWithIds = data.map((item, index) => ({
|
||||
...item,
|
||||
id: `${item.tipoBobina}-${index}` // Asumiendo que tipoBobina es único por reporte o combinar con index
|
||||
}));
|
||||
setReportData(dataWithIds);
|
||||
if (dataWithIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -64,18 +83,35 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
}
|
||||
const dataToExport = reportData.map(item => ({
|
||||
"Tipo Bobina": item.tipoBobina,
|
||||
"Bobinas Iniciales": item.bobinasIniciales,
|
||||
"Cant. Inicial": item.bobinasIniciales,
|
||||
"Kg Iniciales": item.kilosIniciales,
|
||||
"Bobinas Compradas": item.bobinasCompradas,
|
||||
"Compradas": item.bobinasCompradas,
|
||||
"Kg Comprados": item.kilosComprados,
|
||||
"Bobinas Consumidas": item.bobinasConsumidas,
|
||||
"Consumidas": item.bobinasConsumidas,
|
||||
"Kg Consumidos": item.kilosConsumidos,
|
||||
"Bobinas Dañadas": item.bobinasDaniadas,
|
||||
"Dañadas": item.bobinasDaniadas,
|
||||
"Kg Dañados": item.kilosDaniados,
|
||||
"Bobinas Finales": item.bobinasFinales,
|
||||
"Cant. Final": item.bobinasFinales,
|
||||
"Kg Finales": item.kilosFinales,
|
||||
}));
|
||||
|
||||
// Añadir fila de totales
|
||||
const totalesRow = {
|
||||
"Tipo Bobina": "Totales",
|
||||
"Cant. Inicial": reportData.reduce((sum, item) => sum + item.bobinasIniciales, 0),
|
||||
"Kg Iniciales": reportData.reduce((sum, item) => sum + item.kilosIniciales, 0),
|
||||
"Compradas": reportData.reduce((sum, item) => sum + item.bobinasCompradas, 0),
|
||||
"Kg Comprados": reportData.reduce((sum, item) => sum + item.kilosComprados, 0),
|
||||
"Consumidas": reportData.reduce((sum, item) => sum + item.bobinasConsumidas, 0),
|
||||
"Kg Consumidos": reportData.reduce((sum, item) => sum + item.kilosConsumidos, 0),
|
||||
"Dañadas": reportData.reduce((sum, item) => sum + item.bobinasDaniadas, 0),
|
||||
"Kg Dañados": reportData.reduce((sum, item) => sum + item.kilosDaniados, 0),
|
||||
"Cant. Final": reportData.reduce((sum, item) => sum + item.bobinasFinales, 0),
|
||||
"Kg Finales": reportData.reduce((sum, item) => sum + item.kilosFinales, 0),
|
||||
};
|
||||
dataToExport.push(totalesRow);
|
||||
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
const headers = Object.keys(dataToExport[0]);
|
||||
ws['!cols'] = headers.map(h => {
|
||||
@@ -91,7 +127,9 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
XLSX.utils.book_append_sheet(wb, ws, "MovimientoBobinas");
|
||||
let fileName = "ReporteMovimientoBobinas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}_Planta${currentParams.idPlanta}`;
|
||||
// Asumiendo que currentParams.nombrePlanta está disponible o se usa idPlanta
|
||||
fileName += `_${currentParams.nombrePlanta || `Planta${currentParams.idPlanta}`}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
@@ -122,6 +160,137 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
// Definiciones de Columnas para DataGrid
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'tipoBobina', headerName: 'Tipo Bobina', width: 200, flex: 1.5 },
|
||||
{ field: 'bobinasIniciales', headerName: 'Cant. Ini.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosIniciales', headerName: 'Kg Ini.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'bobinasCompradas', headerName: 'Compradas', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosComprados', headerName: 'Kg Compr.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'bobinasConsumidas', headerName: 'Consum.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosConsumidos', headerName: 'Kg Consum.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'bobinasDaniadas', headerName: 'Dañadas', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosDaniados', headerName: 'Kg Dañ.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'bobinasFinales', headerName: 'Cant. Fin.', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'kilosFinales', headerName: 'Kg Finales', type: 'number', width: 110, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const rows = useMemo(() => reportData, [reportData]);
|
||||
|
||||
// Calcular totales para el footer
|
||||
const totales = useMemo(() => {
|
||||
if (reportData.length === 0) return null;
|
||||
return {
|
||||
bobinasIniciales: reportData.reduce((sum, item) => sum + item.bobinasIniciales, 0),
|
||||
kilosIniciales: reportData.reduce((sum, item) => sum + item.kilosIniciales, 0),
|
||||
bobinasCompradas: reportData.reduce((sum, item) => sum + item.bobinasCompradas, 0),
|
||||
kilosComprados: reportData.reduce((sum, item) => sum + item.kilosComprados, 0),
|
||||
bobinasConsumidas: reportData.reduce((sum, item) => sum + item.bobinasConsumidas, 0),
|
||||
kilosConsumidos: reportData.reduce((sum, item) => sum + item.kilosConsumidos, 0),
|
||||
bobinasDaniadas: reportData.reduce((sum, item) => sum + item.bobinasDaniadas, 0),
|
||||
kilosDaniados: reportData.reduce((sum, item) => sum + item.kilosDaniados, 0),
|
||||
bobinasFinales: reportData.reduce((sum, item) => sum + item.bobinasFinales, 0),
|
||||
kilosFinales: reportData.reduce((sum, item) => sum + item.kilosFinales, 0),
|
||||
};
|
||||
}, [reportData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooter = () => {
|
||||
if (!totales) return null;
|
||||
|
||||
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
|
||||
const colConfig = columns.find(c => c.field === field);
|
||||
let targetWidth: number | string = 'auto'; // Por defecto, dejar que el contenido decida
|
||||
let targetMinWidth: number | string = 'auto';
|
||||
|
||||
if (isLabel) {
|
||||
// Para la etiqueta "TOTALES:", un ancho más ajustado.
|
||||
// Podrías basarlo en el ancho de la primera columna si es consistentemente la de "Tipo Bobina"
|
||||
// o un valor fijo que sepas que funciona.
|
||||
targetWidth = colConfig?.width ? Math.max(80, colConfig.width * 0.6) : 120; // Ej: 60% del ancho de la columna o 120px
|
||||
targetMinWidth = 80; // Un mínimo razonable para "TOTALES:"
|
||||
} else if (colConfig) {
|
||||
// Para los valores numéricos, podemos ser un poco más conservadores que el ancho de la columna.
|
||||
// O usar el ancho de la columna si es pequeño.
|
||||
targetWidth = colConfig.width ? Math.max(70, colConfig.width * 0.85) : 90; // Ej: 85% del ancho de la columna o 90px
|
||||
targetMinWidth = 70; // Un mínimo para números
|
||||
}
|
||||
|
||||
return {
|
||||
minWidth: targetMinWidth,
|
||||
width: targetWidth,
|
||||
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'right' | 'left' | 'center',
|
||||
pr: isLabel ? 1 : (field === 'kilosFinales' ? 0 : 1), // padding-right
|
||||
fontWeight: 'bold',
|
||||
// Añadimos overflow y textOverflow para manejar texto largo en la etiqueta si fuera necesario
|
||||
overflow: isLabel ? 'hidden' : undefined,
|
||||
textOverflow: isLabel ? 'ellipsis' : undefined,
|
||||
whiteSpace: 'nowrap', // Asegurar que no haya saltos de línea en los totales
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px',
|
||||
}}>
|
||||
{/* Box para la paginación estándar */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
px:1,
|
||||
// Para asegurar que la paginación no se coma todo el espacio si es muy ancha:
|
||||
// Podríamos darle un flex-basis o un maxWidth si los totales necesitan más espacio garantizado.
|
||||
// Por ejemplo:
|
||||
// flexBasis: '50%', // Ocupa el 50% del espacio disponible si no hay otros factores
|
||||
// maxWidth: '600px', // Un máximo absoluto
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none',
|
||||
width: '100%',
|
||||
'& .MuiToolbar-root': {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Box para los totales personalizados */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap', // Ya estaba, es importante
|
||||
overflowX: 'auto',
|
||||
px:1,
|
||||
flexShrink: 1, // Permitir que este contenedor se encoja si es necesario
|
||||
// maxWidth: 'calc(100% - ANCHO_PAGINACION_ESTIMADO)' // Si quieres ser muy preciso
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasIniciales')}>{numberLocaleFormatter(totales.bobinasIniciales)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosIniciales')}>{numberLocaleFormatter(totales.kilosIniciales)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasCompradas')}>{numberLocaleFormatter(totales.bobinasCompradas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosComprados')}>{numberLocaleFormatter(totales.kilosComprados)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasConsumidas')}>{numberLocaleFormatter(totales.bobinasConsumidas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosConsumidos')}>{numberLocaleFormatter(totales.kilosConsumidos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasDaniadas')}>{numberLocaleFormatter(totales.bobinasDaniadas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosDaniados')}>{numberLocaleFormatter(totales.kilosDaniados)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('bobinasFinales')}>{numberLocaleFormatter(totales.bobinasFinales)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('kilosFinales')}>{numberLocaleFormatter(totales.kilosFinales)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -140,7 +309,7 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Movimiento de Bobinas</Typography>
|
||||
<Typography variant="h5">Reporte: Movimiento de Bobinas {currentParams?.nombrePlanta ? `(${currentParams.nombrePlanta})` : ''}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
onClick={handleGenerarYAbrirPdf}
|
||||
@@ -164,47 +333,24 @@ const ReporteMovimientoBobinasPage: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Bobina</TableCell>
|
||||
<TableCell align="right">Cant. Ini.</TableCell>
|
||||
<TableCell align="right">Kg Ini.</TableCell>
|
||||
<TableCell align="right">Compradas</TableCell>
|
||||
<TableCell align="right">Kg Compr.</TableCell>
|
||||
<TableCell align="right">Consum.</TableCell>
|
||||
<TableCell align="right">Kg Consum.</TableCell>
|
||||
<TableCell align="right">Dañadas</TableCell>
|
||||
<TableCell align="right">Kg Dañ.</TableCell>
|
||||
<TableCell align="right">Cant. Fin.</TableCell>
|
||||
<TableCell align="right">Kg Finales</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.map((row, idx) => (
|
||||
<TableRow key={row.tipoBobina + idx}>
|
||||
<TableCell>{row.tipoBobina}</TableCell>
|
||||
<TableCell align="right">{row.bobinasIniciales.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosIniciales.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.bobinasCompradas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosComprados.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.bobinasConsumidas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosConsumidos.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.bobinasDaniadas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosDaniados.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.bobinasFinales.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.kilosFinales.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{!loading && !error && reportData.length > 0 && (
|
||||
<Paper sx={{ width: '100%', mt: 2 }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: CustomFooter }}
|
||||
density="compact"
|
||||
autoHeight // Para que se ajuste al contenido y al footer
|
||||
hideFooterSelectedRowCount
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { TiradasPublicacionesSeccionesDto } from '../../models/dtos/Reportes/TiradasPublicacionesSeccionesDto';
|
||||
import SeleccionaReporteTiradasPublicacionesSecciones from './SeleccionaReporteTiradasPublicacionesSecciones';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaz extendida para DataGrid
|
||||
interface TiradasPublicacionesSeccionesDataGridDto extends TiradasPublicacionesSeccionesDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<TiradasPublicacionesSeccionesDto[]>([]);
|
||||
const [reportData, setReportData] = useState<TiradasPublicacionesSeccionesDataGridDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -27,6 +33,9 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
mesAnioParaNombreArchivo?: string;
|
||||
} | null>(null);
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined, fractionDigits: number = 0) =>
|
||||
value != null ? Number(value).toLocaleString('es-AR', { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits }) : '';
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
idPublicacion: number;
|
||||
fechaDesde: string;
|
||||
@@ -47,15 +56,22 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
const plantaData = await plantaService.getPlantaById(params.idPlanta);
|
||||
plantaNombre = plantaData?.nombre ?? "N/A";
|
||||
}
|
||||
const mesAnioParts = params.fechaDesde.split('-');
|
||||
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
|
||||
// Formatear mes y año para el título/nombre de archivo
|
||||
// const fechaDesdeObj = new Date(params.fechaDesde + 'T00:00:00'); // Asegurar que es local
|
||||
// const mesAnioNombre = fechaDesdeObj.toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
||||
|
||||
// Usar el formato mes/año del PDF para el nombre del archivo si se prefiere, o el que ya tenías
|
||||
const dateParts = params.fechaDesde.split('-'); // Asume YYYY-MM-DD
|
||||
const mesAnioNombre = `${dateParts[1]}/${dateParts[0]}`;
|
||||
|
||||
|
||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, nombrePlanta: plantaNombre, mesAnioParaNombreArchivo: mesAnioNombre});
|
||||
|
||||
try {
|
||||
const data = await reportesService.getTiradasPublicacionesSecciones(params);
|
||||
setReportData(data);
|
||||
if (data.length === 0) {
|
||||
const dataWithIds = data.map((item, index) => ({ ...item, id: `${item.nombreSeccion}-${index}` }));
|
||||
setReportData(dataWithIds);
|
||||
if (dataWithIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -84,13 +100,24 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
}
|
||||
const dataToExport = reportData.map(item => ({
|
||||
"Nombre Sección": item.nombreSeccion,
|
||||
"Total Páginas Impresas": item.totalPaginasImpresas,
|
||||
"Cantidad Ediciones": item.cantidadTiradas,
|
||||
"Total Páginas x Edición": item.totalPaginasEjemplares,
|
||||
"Páginas Impresas": item.totalPaginasImpresas, // Cambiado para coincidir con PDF
|
||||
"Total Ediciones": item.cantidadTiradas, // Cambiado para coincidir con PDF
|
||||
"Pág. Por Edición (Promedio)": item.totalPaginasEjemplares, // Nombre según PDF
|
||||
"Total Ejemplares": item.totalEjemplares,
|
||||
"Prom. Pág./Ejemplar": item.promedioPaginasPorEjemplar,
|
||||
"Pág. Ejemplar (Promedio)": item.promedioPaginasPorEjemplar, // Nombre según PDF
|
||||
}));
|
||||
|
||||
// Totales para Excel
|
||||
const totales = {
|
||||
"Nombre Sección": "Totales",
|
||||
"Páginas Impresas": reportData.reduce((sum, item) => sum + item.totalPaginasImpresas, 0),
|
||||
"Total Ediciones": reportData.reduce((sum, item) => sum + item.cantidadTiradas, 0),
|
||||
"Pág. Por Edición (Promedio)": reportData.reduce((sum, item) => sum + item.totalPaginasEjemplares, 0), // Suma de promedios para el total, como en el PDF
|
||||
"Total Ejemplares": reportData.reduce((sum, item) => sum + item.totalEjemplares, 0),
|
||||
"Pág. Ejemplar (Promedio)": reportData.reduce((sum, item) => sum + item.promedioPaginasPorEjemplar, 0), // Suma de promedios para el total
|
||||
};
|
||||
dataToExport.push(totales);
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
||||
const headers = Object.keys(dataToExport[0] || {});
|
||||
ws['!cols'] = headers.map(h => {
|
||||
@@ -105,8 +132,8 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
let fileName = "ReporteTiradasSecciones";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
|
||||
if (!currentParams.consolidado) fileName += `_Planta${currentParams.idPlanta}`;
|
||||
else fileName += "_Consolidado";
|
||||
if (!currentParams.consolidado && currentParams.idPlanta) fileName += `_Planta${currentParams.idPlanta}`;
|
||||
else if (currentParams.consolidado) fileName += "_Consolidado";
|
||||
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
@@ -138,6 +165,83 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
const columns: GridColDef<TiradasPublicacionesSeccionesDataGridDto>[] = [
|
||||
{ field: 'nombreSeccion', headerName: 'Nombre', width: 250, flex: 1.5 },
|
||||
{ field: 'totalPaginasImpresas', headerName: 'Páginas Impresas', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'cantidadTiradas', headerName: 'Total Ediciones', type: 'number', width: 130, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'totalPaginasEjemplares', headerName: 'Pág. Por Edición (Promedio)', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'totalEjemplares', headerName: 'Total Ejemplares', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
{ field: 'promedioPaginasPorEjemplar', headerName: 'Pág. Ejemplar (Promedio)', type: 'number', width: 180, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberLocaleFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
const rows = useMemo(() => reportData, [reportData]);
|
||||
|
||||
const totalesGenerales = useMemo(() => {
|
||||
if (reportData.length === 0) return null;
|
||||
return {
|
||||
totalPaginasImpresas: reportData.reduce((sum, item) => sum + item.totalPaginasImpresas, 0),
|
||||
cantidadTiradas: reportData.reduce((sum, item) => sum + item.cantidadTiradas, 0),
|
||||
totalPaginasEjemplares: reportData.reduce((sum, item) => sum + item.totalPaginasEjemplares, 0), // Suma de promedios para el total, como en el PDF
|
||||
totalEjemplares: reportData.reduce((sum, item) => sum + item.totalEjemplares, 0),
|
||||
promedioPaginasPorEjemplar: reportData.reduce((sum, item) => sum + item.promedioPaginasPorEjemplar, 0), // Suma de promedios para el total
|
||||
};
|
||||
}, [reportData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CustomFooter = () => {
|
||||
if (!totalesGenerales) return null;
|
||||
|
||||
const getCellStyle = (field: (typeof columns)[number]['field'] | 'label', isLabel: boolean = false) => {
|
||||
const colConfig = columns.find(c => c.field === field);
|
||||
let targetWidth: number | string = 'auto';
|
||||
let targetMinWidth: number | string = 'auto';
|
||||
|
||||
if (isLabel) {
|
||||
targetWidth = colConfig?.width ? Math.max(100, colConfig.width * 0.6) : 150;
|
||||
targetMinWidth = 100;
|
||||
} else if (colConfig) {
|
||||
targetWidth = colConfig.width ? Math.max(80, colConfig.width * 0.8) : 100;
|
||||
targetMinWidth = 80;
|
||||
}
|
||||
|
||||
return {
|
||||
minWidth: targetMinWidth,
|
||||
width: targetWidth,
|
||||
textAlign: isLabel ? 'left' : (colConfig?.align || 'right') as 'left' | 'right' | 'center',
|
||||
pr: isLabel ? 1 : (field === 'promedioPaginasPorEjemplar' ? 0 : 1),
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
width: '100%', borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, overflow: 'hidden', px:1 }}>
|
||||
<GridFooter
|
||||
sx={{ borderTop: 'none', width: 'auto',
|
||||
'& .MuiToolbar-root': { paddingLeft: 0, paddingRight: 0, },
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'center', fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap', overflowX: 'auto', px:1, flexShrink: 1,
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('label', true)}>Totales</Typography> {/* Cambiado de TOTALES: a Totales */}
|
||||
<Typography variant="subtitle2" sx={getCellStyle('totalPaginasImpresas')}>{numberLocaleFormatter(totalesGenerales.totalPaginasImpresas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('cantidadTiradas')}>{numberLocaleFormatter(totalesGenerales.cantidadTiradas)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('totalPaginasEjemplares')}>{numberLocaleFormatter(totalesGenerales.totalPaginasEjemplares)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('totalEjemplares')}>{numberLocaleFormatter(totalesGenerales.totalEjemplares)}</Typography>
|
||||
<Typography variant="subtitle2" sx={getCellStyle('promedioPaginasPorEjemplar')}>{numberLocaleFormatter(totalesGenerales.promedioPaginasPorEjemplar)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
if (showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -156,7 +260,7 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Tiradas por Publicación y Secciones</Typography>
|
||||
<Typography variant="h5">Reporte: Tiradas por Publicación Mensual</Typography> {/* Título ajustado al PDF */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || reportData.length === 0 || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
@@ -169,39 +273,35 @@ const ReporteTiradasPublicacionesSeccionesPage: React.FC = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Publicación: {currentParams?.nombrePublicacion} |
|
||||
Planta: {currentParams?.nombrePlanta} |
|
||||
Mes Consultado: {currentParams?.mesAnioParaNombreArchivo}
|
||||
</Typography>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre Sección</TableCell>
|
||||
<TableCell align="right">Total Páginas Imp.</TableCell>
|
||||
<TableCell align="right">Cant. Ediciones</TableCell>
|
||||
<TableCell align="right">Total Pág. x Edición</TableCell>
|
||||
<TableCell align="right">Total Ejemplares</TableCell>
|
||||
<TableCell align="right">Prom. Pág./Ejemplar</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{reportData.map((row, idx) => (
|
||||
<TableRow key={`${row.nombreSeccion}-${idx}`}>
|
||||
<TableCell>{row.nombreSeccion}</TableCell>
|
||||
<TableCell align="right">{row.totalPaginasImpresas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.cantidadTiradas.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.totalPaginasEjemplares.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.totalEjemplares.toLocaleString('es-AR')}</TableCell>
|
||||
<TableCell align="right">{row.promedioPaginasPorEjemplar.toLocaleString('es-AR')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Paper sx={{ width: '100%', mt: 2 }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: CustomFooter }}
|
||||
density="compact"
|
||||
sx={{ height: 'calc(100vh - 320px)' }} // Ajustar altura según sea necesario
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: { pageSize: 100, page: 0 },
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[25, 50 , 100]}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
{!loading && !error && reportData.length === 0 && (<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
{!loading && !error && reportData.length === 0 && currentParams && (<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -240,13 +240,14 @@ const ReporteVentaMensualSecretariaPage: React.FC = () => {
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell rowSpan={2} sx={{verticalAlign: 'bottom'}}>Día</TableCell>
|
||||
<TableCell rowSpan={1} sx={{verticalAlign: 'bottom'}}></TableCell>
|
||||
<TableCell colSpan={4} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>El Día</TableCell>
|
||||
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Popular</TableCell>
|
||||
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Clarín</TableCell>
|
||||
<TableCell colSpan={3} align="center" sx={{borderBottom: '1px solid rgba(224, 224, 224, 1)'}}>Nación</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="right">Día</TableCell>
|
||||
<TableCell align="right">Tir. Coop.</TableCell><TableCell align="right">Dev. Coop.</TableCell>
|
||||
<TableCell align="right">Vta. Coop.</TableCell><TableCell align="right">Vta. Can.</TableCell>
|
||||
<TableCell align="right">Tirada</TableCell><TableCell align="right">Devol.</TableCell><TableCell align="right">Venta</TableCell>
|
||||
|
||||
@@ -1,98 +1,243 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, CircularProgress } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
|
||||
const reportesSubModules = [
|
||||
{ label: 'Existencia de Papel', path: 'existencia-papel' },
|
||||
{ label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
|
||||
{ label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
|
||||
{ label: 'Distribución General', path: 'listado-distribucion-general' },
|
||||
{ label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
|
||||
{ label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
|
||||
{ label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
|
||||
{ label: 'Det. Distribución Canillas', path: 'detalle-distribucion-canillas' },
|
||||
{ label: 'Tiradas Pub./Sección', path: 'tiradas-publicaciones-secciones' },
|
||||
{ label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
|
||||
{ label: 'Consumo Bobinas/Pub.', path: 'consumo-bobinas-publicacion' },
|
||||
{ label: 'Comparativa Cons. Bobinas', path: 'comparativa-consumo-bobinas' },
|
||||
{ label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
|
||||
// Definición de los módulos de reporte con sus categorías, etiquetas y rutas
|
||||
const allReportModules: { category: string; label: string; path: string }[] = [
|
||||
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' },
|
||||
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
|
||||
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' },
|
||||
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
|
||||
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' },
|
||||
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
|
||||
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' },
|
||||
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
|
||||
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' },
|
||||
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' },
|
||||
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
|
||||
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' },
|
||||
];
|
||||
|
||||
const predefinedCategoryOrder = [
|
||||
'Balance de Cuentas',
|
||||
'Listados Distribución',
|
||||
'Ctrl. Devoluciones',
|
||||
'Existencia Papel',
|
||||
'Movimientos Bobinas',
|
||||
'Consumos Bobinas',
|
||||
'Tiradas por Publicación',
|
||||
'Secretaría',
|
||||
];
|
||||
|
||||
|
||||
const ReportesIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
|
||||
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
|
||||
|
||||
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/reportes';
|
||||
// Extrae la parte de la ruta que sigue a '/reportes/'
|
||||
const subPathSegment = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Toma solo el primer segmento
|
||||
: undefined;
|
||||
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
|
||||
const subPathSegment = pathParts[0];
|
||||
|
||||
let activeTabIndex = -1;
|
||||
let activeReportFoundInEffect = false;
|
||||
|
||||
if (subPathSegment) {
|
||||
activeTabIndex = reportesSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPathSegment
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
|
||||
const activeReport = allReportModules.find(module => module.path === subPathSegment);
|
||||
if (activeReport) {
|
||||
setExpandedCategory(activeReport.category);
|
||||
activeReportFoundInEffect = true;
|
||||
} else {
|
||||
// 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
|
||||
setExpandedCategory(false);
|
||||
}
|
||||
} else {
|
||||
setSelectedSubTab(false); // Ninguna sub-ruta activa o conocida, o no hay sub-módulos
|
||||
setExpandedCategory(false);
|
||||
}
|
||||
}
|
||||
}, [location.pathname, navigate]); // Solo depende de location.pathname y navigate
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
// No es necesario setSelectedSubTab aquí directamente, el useEffect lo manejará.
|
||||
navigate(reportesSubModules[newValue].path);
|
||||
if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) {
|
||||
let firstReportToNavigate: { category: string; label: string; path: string } | null = null;
|
||||
for (const category of uniqueCategories) {
|
||||
const reportsInCat = allReportModules.filter(r => r.category === category);
|
||||
if (reportsInCat.length > 0) {
|
||||
firstReportToNavigate = reportsInCat[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstReportToNavigate) {
|
||||
navigate(firstReportToNavigate.path, { replace: true });
|
||||
activeReportFoundInEffect = true;
|
||||
}
|
||||
}
|
||||
// Solo se establece a false si no estamos en el proceso de navegación inicial O si no se encontró reporte
|
||||
if (!activeReportFoundInEffect || location.pathname !== currentBasePath) {
|
||||
setIsLoadingInitialNavigation(false);
|
||||
}
|
||||
|
||||
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]);
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
|
||||
};
|
||||
|
||||
// Si no hay sub-módulos definidos, podría ser un estado inicial
|
||||
if (reportesSubModules.length === 0) {
|
||||
const handleReportClick = (reportPath: string) => {
|
||||
navigate(reportPath);
|
||||
};
|
||||
|
||||
const isReportActive = (reportPath: string) => {
|
||||
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`);
|
||||
};
|
||||
|
||||
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader
|
||||
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
|
||||
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h5" gutterBottom>Módulo de Reportes</Typography>
|
||||
<Typography>No hay reportes configurados.</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Módulo de Reportes
|
||||
</Typography>
|
||||
<Paper square elevation={1}>
|
||||
<Tabs
|
||||
value={selectedSubTab} // 'false' es un valor válido para Tabs si ninguna pestaña está seleccionada
|
||||
onChange={handleSubTabChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos de reportes"
|
||||
// Contenedor principal que se adaptará a su padre
|
||||
// Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
|
||||
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
|
||||
{/* Panel Lateral para Navegación */}
|
||||
<Paper
|
||||
elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
|
||||
square // Bordes rectos
|
||||
sx={{
|
||||
width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
|
||||
minWidth: { xs: 200, sm: 220 },
|
||||
height: '100%', // Ocupa toda la altura del Box padre
|
||||
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
overflowY: 'auto',
|
||||
bgcolor: 'background.paper', // O el color que desees para el menú
|
||||
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical
|
||||
}}
|
||||
>
|
||||
{reportesSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
{/* 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
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
|
||||
Reportes
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Lista de Categorías y Reportes */}
|
||||
{uniqueCategories.length > 0 ? (
|
||||
<List component="nav" dense sx={{ pt: 0 }} /* Quitar padding superior de la lista si el título ya lo tiene */ >
|
||||
{uniqueCategories.map((category) => {
|
||||
const reportsInCategory = allReportModules.filter(r => r.category === category);
|
||||
const isExpanded = expandedCategory === category;
|
||||
|
||||
return (
|
||||
<React.Fragment key={category}>
|
||||
<ListItemButton
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
sx={{
|
||||
// py: 1.2, // Ajustar padding vertical de items de categoría
|
||||
// backgroundColor: isExpanded ? 'action.selected' : 'transparent',
|
||||
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
|
||||
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={category}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: isExpanded ? 'bold' : 'normal',
|
||||
// color: isExpanded ? 'primary.main' : 'text.primary'
|
||||
}}
|
||||
/>
|
||||
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
|
||||
</ListItemButton>
|
||||
{reportsInCategory.length > 0 && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding dense>
|
||||
{reportsInCategory.map((report) => (
|
||||
<ListItemButton
|
||||
key={report.path}
|
||||
selected={isReportActive(report.path)}
|
||||
onClick={() => handleReportClick(report.path)}
|
||||
sx={{
|
||||
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título)
|
||||
py: 0.8, // Padding vertical de items de reporte
|
||||
...(isReportActive(report.path) && {
|
||||
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil
|
||||
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo
|
||||
'& .MuiListItemText-primary': {
|
||||
fontWeight: 'medium', // O 'bold'
|
||||
// color: 'primary.main'
|
||||
},
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.palette.action.hover
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</Tabs>
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
{reportsInCategory.length === 0 && isExpanded && (
|
||||
<ListItemText
|
||||
primary="No hay reportes en esta categoría."
|
||||
sx={{ pl: 3.5, fontStyle: 'italic', color: 'text.secondary', py:1, typography: 'body2' }}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
) : (
|
||||
<Typography sx={{p:2, fontStyle: 'italic'}}>No hay categorías configuradas.</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
{/* Outlet renderizará ReporteExistenciaPapelPage u otros
|
||||
Solo renderiza el Outlet si hay una pestaña seleccionada VÁLIDA.
|
||||
Si selectedSubTab es 'false' (porque ninguna ruta coincide con los sub-módulos),
|
||||
se muestra el mensaje.
|
||||
*/}
|
||||
{selectedSubTab !== false ? <Outlet /> : <Typography sx={{p:2}}>Seleccione un reporte del menú lateral o de las pestañas.</Typography>}
|
||||
|
||||
{/* Área Principal para el Contenido del Reporte */}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1, // Ocupa el espacio restante
|
||||
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo
|
||||
overflowY: 'auto',
|
||||
height: '100%', // Ocupa toda la altura del Box padre
|
||||
bgcolor: 'grey.100' // Un color de fondo diferente para distinguir el área de contenido
|
||||
}}
|
||||
>
|
||||
{/* El Outlet renderiza el componente del reporte específico */}
|
||||
{(!location.pathname.startsWith('/reportes/') || !allReportModules.some(r => isReportActive(r.path))) && location.pathname !== '/reportes/' && location.pathname !== '/reportes' && !isLoadingInitialNavigation && (
|
||||
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
||||
El reporte solicitado no existe o la ruta no es válida.
|
||||
</Typography>
|
||||
)}
|
||||
{(location.pathname === '/reportes/' || location.pathname === '/reportes') && !allReportModules.some(r => isReportActive(r.path)) && !isLoadingInitialNavigation && (
|
||||
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
||||
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."}
|
||||
</Typography>
|
||||
)}
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||
FormControl, InputLabel, Select, MenuItem,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton
|
||||
} from '@mui/material';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
@@ -116,17 +118,65 @@ const SeleccionaReporteListadoDistribucionCanillasImporte: React.FC<SeleccionaRe
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={esAccionista}
|
||||
onChange={(e) => setEsAccionista(e.target.checked)}
|
||||
<Box sx={{ mt: 2, mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
|
||||
Tipo de reporte
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={esAccionista ? 'accionistas' : 'canillitas'}
|
||||
exclusive
|
||||
onChange={(_, value) => {
|
||||
if (value !== null) setEsAccionista(value === 'accionistas');
|
||||
}}
|
||||
aria-label="Tipo de reporte"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label="Ver Accionistas"
|
||||
sx={{ mt: 1, mb: 1 }}
|
||||
/>
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
boxShadow: 2,
|
||||
backgroundColor: '#f5f5f5',
|
||||
p: 0.5,
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
value="canillitas"
|
||||
aria-label="Canillitas"
|
||||
sx={{
|
||||
fontWeight: esAccionista ? 400 : 700,
|
||||
bgcolor: !esAccionista ? 'primary.main' : 'background.paper',
|
||||
color: !esAccionista ? 'primary.contrastText' : 'text.primary',
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
minWidth: 140,
|
||||
borderRadius: 2,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
Canillitas
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
value="accionistas"
|
||||
aria-label="Accionistas"
|
||||
sx={{
|
||||
fontWeight: esAccionista ? 700 : 400,
|
||||
bgcolor: esAccionista ? 'primary.main' : 'background.paper',
|
||||
color: esAccionista ? 'primary.contrastText' : 'text.primary',
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
minWidth: 140,
|
||||
borderRadius: 2,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
Accionistas
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
@@ -67,6 +67,8 @@ import ReporteConsumoBobinasSeccionPage from '../pages/Reportes/ReporteConsumoBo
|
||||
import ReporteConsumoBobinasPublicacionPage from '../pages/Reportes/ReporteConsumoBobinasPublicacionPage';
|
||||
import ReporteComparativaConsumoBobinasPage from '../pages/Reportes/ReporteComparativaConsumoBobinasPage';
|
||||
import ReporteCuentasDistribuidoresPage from '../pages/Reportes/ReporteCuentasDistribuidoresPage';
|
||||
import ReporteListadoDistribucionPage from '../pages/Reportes/ReporteListadoDistribucionPage';
|
||||
import ReporteControlDevolucionesPage from '../pages/Reportes/ReporteControlDevolucionesPage';
|
||||
|
||||
// Auditorias
|
||||
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
||||
@@ -177,6 +179,8 @@ const AppRoutes = () => {
|
||||
<Route path="consumo-bobinas-publicacion" element={<ReporteConsumoBobinasPublicacionPage />} />
|
||||
<Route path="comparativa-consumo-bobinas" element={<ReporteComparativaConsumoBobinasPage />} />
|
||||
<Route path="cuentas-distribuidores" element={<ReporteCuentasDistribuidoresPage />} />
|
||||
<Route path="listado-distribucion-distribuidores" element={<ReporteListadoDistribucionPage />} />
|
||||
<Route path="control-devoluciones" element={<ReporteControlDevolucionesPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Radios (anidado) */}
|
||||
|
||||
@@ -14,6 +14,8 @@ import type { ConsumoBobinasSeccionDto } from '../../models/dtos/Reportes/Consum
|
||||
import type { ConsumoBobinasPublicacionDto } from '../../models/dtos/Reportes/ConsumoBobinasPublicacionDto';
|
||||
import type { ComparativaConsumoBobinasDto } from '../../models/dtos/Reportes/ComparativaConsumoBobinasDto';
|
||||
import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Reportes/ReporteCuentasDistribuidorResponseDto';
|
||||
import type { ListadoDistribucionDistribuidoresResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionDistribuidoresResponseDto';
|
||||
import type { ControlDevolucionesDataResponseDto } from '../../models/dtos/Reportes/ControlDevolucionesDataResponseDto';
|
||||
|
||||
interface GetExistenciaPapelParams {
|
||||
fechaDesde: string; // yyyy-MM-dd
|
||||
@@ -322,6 +324,48 @@ const getListadoDistribucionCanillasImporte = async (params: {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getListadoDistribucionDistribuidores = async (params: {
|
||||
idDistribuidor: number;
|
||||
idPublicacion: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}): Promise<ListadoDistribucionDistribuidoresResponseDto> => {
|
||||
const response = await apiClient.get<ListadoDistribucionDistribuidoresResponseDto>('/reportes/listado-distribucion-distribuidores', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getListadoDistribucionDistribuidoresPdf = async (params: {
|
||||
idDistribuidor: number;
|
||||
idPublicacion: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}): Promise<Blob> => {
|
||||
const response = await apiClient.get('/reportes/listado-distribucion-distribuidores/pdf', {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getControlDevolucionesData = async (params: {
|
||||
fecha: string; // YYYY-MM-DD
|
||||
idEmpresa: number;
|
||||
}): Promise<ControlDevolucionesDataResponseDto> => {
|
||||
const response = await apiClient.get<ControlDevolucionesDataResponseDto>('/reportes/control-devoluciones', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getControlDevolucionesPdf = async (params: {
|
||||
fecha: string; // YYYY-MM-DD
|
||||
idEmpresa: number;
|
||||
}): Promise<Blob> => {
|
||||
const response = await apiClient.get('/reportes/control-devoluciones/pdf', {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const reportesService = {
|
||||
getExistenciaPapel,
|
||||
getExistenciaPapelPdf,
|
||||
@@ -353,6 +397,10 @@ const reportesService = {
|
||||
getComparativaConsumoBobinasPdf,
|
||||
getReporteCuentasDistribuidor,
|
||||
getReporteCuentasDistribuidorPdf,
|
||||
getListadoDistribucionDistribuidores,
|
||||
getListadoDistribucionDistribuidoresPdf,
|
||||
getControlDevolucionesData,
|
||||
getControlDevolucionesPdf,
|
||||
};
|
||||
|
||||
export default reportesService;
|
||||
Reference in New Issue
Block a user