From 2c584e9383a5267c64c47cd036f769d6374a4b9a Mon Sep 17 00:00:00 2001 From: dmolinari Date: Tue, 4 Nov 2025 11:51:43 -0300 Subject: [PATCH] =?UTF-8?q?feat(reportes):=20Permite=20consulta=20consolid?= =?UTF-8?q?ada=20en=20Detalle=20de=20Distribuci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa la funcionalidad para generar el reporte "Detalle de Distribución de Canillas" de forma consolidada para todas las empresas, separando entre Canillitas y Accionistas. Adicionalmente, se realizan correcciones y mejoras visuales en otros reportes. ### Cambios Principales - **Frontend (`ReporteDetalleDistribucionCanillasPage`):** - Se añade la opción "TODAS" al selector de Empresas. - Al seleccionar "TODAS", se muestra un nuevo control para elegir entre "Canillitas" o "Accionistas". - La vista del reporte se simplifica en el modo "TODAS", mostrando solo la tabla correspondiente y ocultando el resumen por tipo de vendedor. - **Backend (`ReportesService`, `ReportesRepository`):** - Se modifica el servicio para recibir el parámetro `esAccionista`. - Se implementa una nueva lógica que, si `idEmpresa` es 0, llama a los nuevos procedimientos almacenados que consultan todas las empresas. - Se ajusta el `ReportesController` para aceptar y pasar el nuevo parámetro. ### Correcciones - **Base de Datos:** - Se añade la función SQL `FN_ObtenerPrecioVigente` para los nuevos Stored Procedures. - Se crean los Stored Procedures `SP_DistCanillasEntradaSalidaPubli_AllEmpresas` y `SP_DistCanillasAccEntradaSalidaPubli_AllEmpresas`. - **Backend (`ReportesController`):** - Se corrigen errores de compilación (`CS7036`, `CS8130`) añadiendo el parámetro `esAccionista` faltante en las llamadas al servicio `ObtenerReporteDistribucionCanillasAsync`. ### Mejoras Visuales - **PDF (`ControlDevolucionesDocument.cs`):** - Se ajustan los espaciados verticales (`Padding` y `Spacing`) en la plantilla QuestPDF del reporte "Control de Devoluciones" para lograr un diseño más compacto. --- .../ControlDevolucionesDocument.cs | 41 +++--- .../Reportes/ReportesController.cs | 29 ++-- .../Reportes/IReportesRepository.cs | 2 + .../Reportes/ReportesRepository.cs | 34 +++++ .../GestionIntegral.Api.csproj | 3 +- .../Services/Reportes/IReportesService.cs | 20 +-- .../Services/Reportes/ReportesService.cs | 76 +++++++---- ...ReporteDetalleDistribucionCanillasPage.tsx | 124 ++++++++++-------- ...ionaReporteDetalleDistribucionCanillas.tsx | 56 ++++---- .../src/services/Reportes/reportesService.ts | 46 +++++-- 10 files changed, 282 insertions(+), 149 deletions(-) diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ControlDevolucionesDocument.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ControlDevolucionesDocument.cs index 85dd057..baed005 100644 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ControlDevolucionesDocument.cs +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/PdfTemplates/ControlDevolucionesDocument.cs @@ -41,9 +41,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates void ComposeContent(IContainer container) { - container.PaddingTop(1, Unit.Centimetre).Column(column => + // << CAMBIO: Reducido el padding superior de 1cm a 5mm >> + container.PaddingTop(5, Unit.Millimetre).Column(column => { - column.Spacing(15); + // << CAMBIO: Reducido el espaciado principal entre elementos de 15 a 10 puntos >> + column.Spacing(10); column.Item().Row(row => { @@ -59,23 +61,24 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates }); }); - column.Item().PaddingTop(5).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold(); + column.Item().PaddingTop(3).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold(); - column.Item().Border(1).Padding(10).Column(innerCol => + column.Item().Border(1).Padding(8).Column(innerCol => // << CAMBIO: Padding reducido de 10 a 8 >> { - innerCol.Spacing(5); + // << CAMBIO: Reducido el espaciado interno de 5 a 4 >> + innerCol.Spacing(4); - // Fila de "Ingresados por Remito" con borde inferior sólido. innerCol.Item().BorderBottom(1, Unit.Point).BorderColor(Colors.Grey.Medium).Row(row => { row.RelativeItem().Text("Ingresados por Remito:").SemiBold(); row.RelativeItem().AlignRight().Text(Model.TotalIngresadosPorRemito.ToString("N0")); - }); // <-- SOLUCIÓN: Borde sólido simple. + }); foreach (var item in Model.Detalles) { var totalSeccion = item.Devueltos - item.Llevados; - innerCol.Item().PaddingTop(5).Row(row => + // << CAMBIO: Reducido el padding superior de 5 a 3 >> + innerCol.Item().PaddingTop(3).Row(row => { row.ConstantItem(100).Text(item.Tipo).SemiBold(); @@ -90,7 +93,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates r.RelativeItem().Text("Devueltos"); r.RelativeItem().AlignRight().Text($"{item.Devueltos:N0}"); }); - sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).Row(r => { + // << CAMBIO: Reducido el padding superior de 2 a 1 >> + sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(1).Row(r => { r.RelativeItem().Text(t => t.Span("Total").SemiBold()); r.RelativeItem().AlignRight().Text(t => t.Span(totalSeccion.ToString("N0")).SemiBold()); }); @@ -99,7 +103,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates } }); - column.Item().PaddingTop(10).Column(finalCol => + // << CAMBIO: Reducido el padding superior de 10 a 8 >> + column.Item().PaddingTop(8).Column(finalCol => { finalCol.Spacing(2); @@ -112,13 +117,15 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates if (isBold) valueText.SemiBold(); }; - // Usamos bordes superiores para separar las líneas de total - finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false)); - finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false)); - finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false)); - finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(5).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false)); - finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false)); - finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(5).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true)); + // << CAMBIO: Reducido el padding superior de 2 a 1 en las siguientes líneas >> + finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false)); + finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false)); + finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false)); + // << CAMBIO: Reducido el padding superior de 5 a 3 >> + finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(3).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false)); + finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false)); + // << CAMBIO: Reducido el padding superior de 5 a 3 >> + finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(3).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true)); }); }); } diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs index 25e3bf4..7ce8088 100644 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs @@ -678,13 +678,18 @@ namespace GestionIntegral.Api.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetReporteDistribucionCanillasData([FromQuery] DateTime fecha, [FromQuery] int idEmpresa) + public async Task GetReporteDistribucionCanillasData( + [FromQuery] DateTime fecha, + [FromQuery] int idEmpresa, + [FromQuery] bool? esAccionista + ) { if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid(); + // Pasar el nuevo parámetro al servicio var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq, ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) = - await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); + await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista); if (error != null) return BadRequest(new { message = error }); @@ -719,14 +724,20 @@ namespace GestionIntegral.Api.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetReporteDistribucionCanillasPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa, [FromQuery] bool soloTotales = false) + public async Task GetReporteDistribucionCanillasPdf( + [FromQuery] DateTime fecha, + [FromQuery] int idEmpresa, + [FromQuery] bool? esAccionista, + [FromQuery] bool soloTotales = false + ) { if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid(); + // Pasar el nuevo parámetro al servicio var ( canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq, remitos, ctrlDevoluciones, _, error - ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); + ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista); if (error != null) return BadRequest(new { message = error }); @@ -795,11 +806,11 @@ namespace GestionIntegral.Api.Controllers _, // canillasAll _, // canillasFechaLiq _, // canillasAccFechaLiq - ctrlDevolucionesRemitosData, // Para SP_ObtenerCtrlDevoluciones -> DataSet "DSObtenerCtrlDevoluciones" - ctrlDevolucionesParaDistCanData, // Para SP_DistCanillasCantidadEntradaSalida -> DataSet "DSCtrlDevoluciones" - ctrlDevolucionesOtrosDiasData, // Para SP_DistCanillasCantidadEntradaSalidaOtrosDias -> DataSet "DSCtrlDevolucionesOtrosDias" + ctrlDevolucionesRemitosData, + ctrlDevolucionesParaDistCanData, + ctrlDevolucionesOtrosDiasData, error - ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); // Reutilizamos este método + ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null); if (error != null) return BadRequest(new { message = error }); @@ -833,7 +844,7 @@ namespace GestionIntegral.Api.Controllers var ( _, _, _, _, _, // Datos no utilizados remitos, detalles, otrosDias, error - ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); + ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null); if (error != null) return BadRequest(new { message = error }); diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs index 9419497..8f38f42 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs @@ -48,5 +48,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes Task> GetDatosReportePublicidadAsync(string periodo); Task> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta); Task> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta); + Task> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha); + Task> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs index a9aa80e..cd72754 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs @@ -653,5 +653,39 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes return Enumerable.Empty(); } } + + public async Task> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha) + { + const string spName = "dbo.SP_DistCanillasEntradaSalidaPubli_AllEmpresas"; + var parameters = new DynamicParameters(); + parameters.Add("@fecha", fecha, DbType.DateTime); + try + { + using var connection = _dbConnectionFactory.CreateConnection(); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error SP {SPName}", spName); + return Enumerable.Empty(); + } + } + + public async Task> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha) + { + const string spName = "dbo.SP_DistCanillasAccEntradaSalidaPubli_AllEmpresas"; + var parameters = new DynamicParameters(); + parameters.Add("@fecha", fecha, DbType.DateTime); + try + { + using var connection = _dbConnectionFactory.CreateConnection(); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error SP {SPName}", spName); + return Enumerable.Empty(); + } + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj index 808f888..0d72e26 100644 --- a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj +++ b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj @@ -1,9 +1,10 @@ - + net9.0 enable enable + 4923d7ee-0944-456c-abcd-d6ce13ba8485 diff --git a/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs b/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs index ff2975a..4432186 100644 --- a/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs +++ b/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs @@ -27,16 +27,16 @@ namespace GestionIntegral.Api.Services.Reportes // Reporte Distribucion Canillas (MC005) - Este es un reporte más complejo Task<( - IEnumerable Canillas, - IEnumerable CanillasAcc, - IEnumerable CanillasAll, - IEnumerable CanillasFechaLiq, - IEnumerable CanillasAccFechaLiq, - IEnumerable CtrlDevolucionesRemitos, // Para SP_ObtenerCtrlDevoluciones - IEnumerable CtrlDevolucionesParaDistCan, // Para SP_DistCanillasCantidadEntradaSalida - IEnumerable CtrlDevolucionesOtrosDias, // <--- NUEVO para SP_DistCanillasCantidadEntradaSalidaOtrosDias - string? Error - )> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa); + IEnumerable Canillas, + IEnumerable CanillasAcc, + IEnumerable CanillasAll, + IEnumerable CanillasFechaLiq, + IEnumerable CanillasAccFechaLiq, + IEnumerable CtrlDevolucionesRemitos, + IEnumerable CtrlDevolucionesParaDistCan, + IEnumerable CtrlDevolucionesOtrosDias, + string? Error + )> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista); // Reporte Tiradas por Publicación y Secciones (RR008) Task<(IEnumerable Data, string? Error)> ObtenerTiradasPublicacionesSeccionesAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta, int idPlanta); diff --git a/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs b/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs index 43282c2..1124351 100644 --- a/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs +++ b/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs @@ -218,30 +218,66 @@ namespace GestionIntegral.Api.Services.Reportes } public async Task<( - IEnumerable Canillas, - IEnumerable CanillasAcc, - IEnumerable CanillasAll, - IEnumerable CanillasFechaLiq, - IEnumerable CanillasAccFechaLiq, - IEnumerable CtrlDevolucionesRemitos, - IEnumerable CtrlDevolucionesParaDistCan, - IEnumerable CtrlDevolucionesOtrosDias, - string? Error - )> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa) + IEnumerable Canillas, + IEnumerable CanillasAcc, + IEnumerable CanillasAll, + IEnumerable CanillasFechaLiq, + IEnumerable CanillasAccFechaLiq, + IEnumerable CtrlDevolucionesRemitos, + IEnumerable CtrlDevolucionesParaDistCan, + IEnumerable CtrlDevolucionesOtrosDias, + string? Error +)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista) { try { - var canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa); - var canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa); + // Función helper para convertir fechas a UTC + Func, IEnumerable> toUtc = + items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList() + ?? Enumerable.Empty(); + + // --- NUEVA LÓGICA PARA "TODAS LAS EMPRESAS" --- + if (idEmpresa == 0) + { + Task> canillasTask = Task.FromResult(Enumerable.Empty()); + Task> canillasAccTask = Task.FromResult(Enumerable.Empty()); + + if (esAccionista == true) // Solo accionistas + { + canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(fecha); + } + else // Solo canillitas (o si es null, por defecto canillitas) + { + canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(fecha); + } + + await Task.WhenAll(canillasTask, canillasAccTask); + + return ( + toUtc(await canillasTask), + toUtc(await canillasAccTask), + Enumerable.Empty(), // El resumen no aplica + Enumerable.Empty(), // Liquidaciones de otras fechas no aplican en esta vista simplificada + Enumerable.Empty(), + Enumerable.Empty(), // Control de devoluciones no aplica + Enumerable.Empty(), + Enumerable.Empty(), + null + ); + } + + // --- LÓGICA ORIGINAL PARA UNA EMPRESA ESPECÍFICA --- + var canillasTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa); + var canillasAccTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa); var canillasAllTask = _reportesRepository.GetDetalleDistribucionCanillasAllPubliAsync(fecha, idEmpresa); var canillasFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasPubliFechaLiqAsync(fecha, idEmpresa); var canillasAccFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliFechaLiqAsync(fecha, idEmpresa); - var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa); // SP_ObtenerCtrlDevoluciones - var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalida - var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalidaOtrosDias + var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa); + var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa); + var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa); await Task.WhenAll( - canillasTask, canillasAccTask, canillasAllTask, + canillasTaskOriginal, canillasAccTaskOriginal, canillasAllTask, canillasFechaLiqTask, canillasAccFechaLiqTask, ctrlDevolucionesRemitosTask, ctrlDevolucionesParaDistCanTask, ctrlDevolucionesOtrosDiasTask @@ -250,13 +286,9 @@ namespace GestionIntegral.Api.Services.Reportes var detallesOriginales = await ctrlDevolucionesParaDistCanTask ?? Enumerable.Empty(); var detallesOrdenados = detallesOriginales.OrderBy(d => d.Tipo).ToList(); - Func, IEnumerable> toUtc = - items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList() - ?? Enumerable.Empty(); - return ( - toUtc(await canillasTask), - toUtc(await canillasAccTask), + toUtc(await canillasTaskOriginal), + toUtc(await canillasAccTaskOriginal), await canillasAllTask ?? Enumerable.Empty(), toUtc(await canillasFechaLiqTask), toUtc(await canillasAccFechaLiqTask), diff --git a/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx b/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx index 41d9b5a..c9558f1 100644 --- a/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx +++ b/Frontend/src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx @@ -8,6 +8,7 @@ import reportesService from '../../services/Reportes/reportesService'; import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto'; import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas'; import * as XLSX from 'xlsx'; +import { usePermissions } from '../../hooks/usePermissions'; import axios from 'axios'; // Para el tipo del footer en DataGridSectionProps @@ -81,9 +82,12 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const [currentParams, setCurrentParams] = useState<{ fecha: string; idEmpresa: number; + esAccionista: boolean; nombreEmpresa?: string; } | null>(null); const [pdfSoloTotales, setPdfSoloTotales] = useState(false); + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVerReporte = isSuperAdmin || tienePermiso("MC005"); const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 }; const [totalesCanillas, setTotalesCanillas] = useState(initialTotals); @@ -115,16 +119,29 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const handleGenerarReporte = useCallback(async (params: { fecha: string; idEmpresa: number; + esAccionista: boolean; }) => { + if (!puedeVerReporte) { + setError("No tiene permiso para generar este reporte."); + setLoading(false); + return; + } + 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 + let nombreEmpresa = `Empresa ID ${params.idEmpresa}`; + if (params.idEmpresa !== 0) { + const empresaService = (await import('../../services/Distribucion/empresaService')).default; + const empData = await empresaService.getEmpresaById(params.idEmpresa); + nombreEmpresa = empData?.nombre ?? nombreEmpresa; + } else { + nombreEmpresa = "TODAS"; + } + + setCurrentParams({ ...params, nombreEmpresa }); + setReportData(null); setTotalesCanillas(initialTotals); setTotalesAccionistas(initialTotals); setTotalesCanillasOtraFecha(initialTotals); @@ -140,7 +157,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const processedData = { canillas: addIds(data.canillas, 'can'), canillasAccionistas: addIds(data.canillasAccionistas, 'acc'), - canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection + canillasTodos: addIds(data.canillasTodos, 'all'), canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'), canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'), controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'), @@ -167,7 +184,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { } finally { setLoading(false); } - }, []); + }, [puedeVerReporte]); const handleVolverAParametros = useCallback(() => { setShowParamSelector(true); @@ -188,10 +205,9 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { if (data && data.length > 0) { const exportedData = data.map(item => { const row: Record = {}; - // Excluir el 'id' generado para DataGrid si existe const { id, ...itemData } = item; Object.keys(fields).forEach(key => { - row[fields[key]] = (itemData as any)[key]; // Usar itemData + 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' }); } @@ -215,18 +231,18 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { } }; - // 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); + if (currentParams?.idEmpresa !== 0) { + formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos); + } formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq); formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq); - let fileName = "ReporteDetalleDistribucionCanillitas"; if (currentParams) { fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`; @@ -265,8 +281,6 @@ const ReporteDetalleDistribucionCanillasPage: 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 }, @@ -295,8 +309,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { { 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 createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { 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); @@ -310,10 +323,9 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { }; }; - // eslint-disable-next-line react/display-name - const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props - ( + `1px solid ${theme.palette.divider}`, minHeight: '52px', }}> @@ -339,6 +351,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { return FooterComponent; }; + // Usar los componentes creados con useMemo const FooterCanillas = useMemo(() => createCustomFooterComponent(totalesCanillas, commonColumns), [totalesCanillas]); const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]); const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]); @@ -346,12 +359,16 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { const FooterResumen = useMemo(() => createCustomFooterComponent(totalesResumen, columnsTodos), [totalesResumen, columnsTodos]); if (showParamSelector) { + if (!loading && !puedeVerReporte) { + return No tiene permiso para acceder a este reporte.; + } + return ( @@ -360,17 +377,45 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { ); } + const renderContent = () => { + if (loading) return ; + if (error && !loading) return {error}; + if (!reportData) return No se encontraron datos.; + + if (currentParams?.idEmpresa === 0) { + if (currentParams.esAccionista) { + return ; + } + return ; + } + + return ( + <> + + + + {reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 && + } + {reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 && + } + + ); + }; + + return ( - Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''} + Reporte: Detalle Distribución ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''} + {currentParams?.idEmpresa !== 0 && ( + + )} - @@ -379,34 +424,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => { - - {loading && } - {error && !loading && {error}} - - {!loading && !error && reportData && ( - <> - - - - - - {reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 && - } - - {reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 && - } - - )} - {!loading && !error && reportData && - Object.values(reportData).every(arr => !arr || arr.length === 0) && - No se encontraron datos para los criterios seleccionados. - } + {renderContent()} ); }; diff --git a/Frontend/src/pages/Reportes/SeleccionaReporteDetalleDistribucionCanillas.tsx b/Frontend/src/pages/Reportes/SeleccionaReporteDetalleDistribucionCanillas.tsx index 0059c17..896c3a6 100644 --- a/Frontend/src/pages/Reportes/SeleccionaReporteDetalleDistribucionCanillas.tsx +++ b/Frontend/src/pages/Reportes/SeleccionaReporteDetalleDistribucionCanillas.tsx @@ -1,16 +1,16 @@ import React, { useState, useEffect } from 'react'; import { Box, Typography, TextField, Button, CircularProgress, Alert, - FormControl, InputLabel, Select, MenuItem + FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup, ToggleButton } from '@mui/material'; -import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; +import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto'; import empresaService from '../../services/Distribucion/empresaService'; interface SeleccionaReporteDetalleDistribucionCanillasProps { onGenerarReporte: (params: { fecha: string; idEmpresa: number; - // soloTotales: boolean; // Podríamos añadirlo si el usuario elige la versión del PDF + esAccionista: boolean; // Añadimos este parámetro }) => Promise; onCancel: () => void; isLoading?: boolean; @@ -24,9 +24,9 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC { const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); const [idEmpresa, setIdEmpresa] = useState(''); - // const [soloTotales, setSoloTotales] = useState(false); // Si se añade la opción + const [esAccionista, setEsAccionista] = useState(false); // Nuevo estado - const [empresas, setEmpresas] = useState([]); + const [empresas, setEmpresas] = useState([]); const [loadingDropdowns, setLoadingDropdowns] = useState(false); const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); @@ -34,8 +34,9 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC { setLoadingDropdowns(true); try { - const data = await empresaService.getAllEmpresas(); // Asume que este servicio existe - setEmpresas(data); + const data = await empresaService.getEmpresasDropdown(); + // Añadimos la opción "TODAS" al principio + setEmpresas([{ idEmpresa: 0, nombre: 'TODAS' }, ...data]); } catch (error) { console.error("Error al cargar empresas:", error); setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar empresas.' })); @@ -49,7 +50,8 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC { const errors: { [key: string]: string | null } = {}; if (!fecha) errors.fecha = 'La fecha es obligatoria.'; - if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.'; + // El idEmpresa ya no puede estar vacío, porque se preselecciona "TODAS" o una empresa + if (idEmpresa === '') errors.idEmpresa = 'Debe seleccionar una empresa.'; setLocalErrors(errors); return Object.keys(errors).length === 0; }; @@ -59,14 +61,14 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC + - Parámetros: Detalle Distribución Canillitas + Parámetros: Detalle Distribución Canillas { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }} > - Seleccione una empresa {empresas.map((e) => ( {e.nombre} ))} {localErrors.idEmpresa && {localErrors.idEmpresa}} - {/* - setSoloTotales(e.target.checked)} + + {/* Selector condicional para Canillitas/Accionistas */} + {idEmpresa === 0 && ( + + + Mostrar para todas las empresas + + { if (value !== null) setEsAccionista(value === 'accionistas'); }} + aria-label="Tipo de Vendedor" disabled={isLoading} - /> - } - label="Generar solo resumen de totales (PDF)" - sx={{ mt: 1, mb: 1 }} - /> - */} + color="primary" + > + Canillitas + Accionistas + + + )} {apiErrorMessage && {apiErrorMessage}} {localErrors.dropdowns && {localErrors.dropdowns}} diff --git a/Frontend/src/services/Reportes/reportesService.ts b/Frontend/src/services/Reportes/reportesService.ts index 9f0aa14..000faf0 100644 --- a/Frontend/src/services/Reportes/reportesService.ts +++ b/Frontend/src/services/Reportes/reportesService.ts @@ -20,6 +20,7 @@ import type { NovedadesCanillasReporteDto } from '../../models/dtos/Reportes/Nov import type { CanillaGananciaReporteDto } from '../../models/dtos/Reportes/CanillaGananciaReporteDto'; import type { ListadoDistCanMensualDiariosDto } from '../../models/dtos/Reportes/ListadoDistCanMensualDiariosDto'; import type { ListadoDistCanMensualPubDto } from '../../models/dtos/Reportes/ListadoDistCanMensualPubDto'; +import axios from 'axios'; interface GetExistenciaPapelParams { fechaDesde: string; // yyyy-MM-dd @@ -209,24 +210,43 @@ const getVentaMensualSecretariaTirDevoPdf = async (params: { fechaDesde: string; return response.data; }; -const getReporteDistribucionCanillas = async (params: { - fecha: string; - idEmpresa: number; +const getReporteDistribucionCanillas = async (params: { + fecha: string; + idEmpresa: number; + esAccionista?: boolean; // Hacerlo opcional }): Promise => { - const response = await apiClient.get('/reportes/distribucion-canillas', { params }); - return response.data; + try { + const response = await apiClient.get('/reportes/distribucion-canillas', { params }); + return response.data; + } catch (error) { + console.error('Error al obtener datos del reporte de distribución de canillas:', error); + throw error; + } }; const getReporteDistribucionCanillasPdf = async (params: { - fecha: string; - idEmpresa: number; - soloTotales: boolean; // Nuevo parámetro + fecha: string; + idEmpresa: number; + esAccionista?: boolean; // Opcional + soloTotales: boolean; }): Promise => { - const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { // La ruta no necesita cambiar si el backend lo maneja - params, // soloTotales se enviará como query param si el backend lo espera así - responseType: 'blob', - }); - return response.data; + try { + const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { + params, + responseType: 'blob' + }); + return response.data; + } catch (error) { + console.error('Error al generar PDF del reporte de distribución de canillas:', error); + if (axios.isAxiosError(error) && error.response?.data) { + // Si el error es un JSON dentro de un Blob, hay que leerlo + if (error.response.data.type === 'application/json') { + const errorJson = JSON.parse(await error.response.data.text()); + throw new Error(errorJson.message || "Error al generar PDF"); + } + } + throw error; + } }; const getTiradasPublicacionesSecciones = async (params: {