diff --git a/Backend/GestionIntegral.Api/Controllers/Contables/SaldosController.cs b/Backend/GestionIntegral.Api/Controllers/Contables/SaldosController.cs new file mode 100644 index 0000000..8ff8f25 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Contables/SaldosController.cs @@ -0,0 +1,109 @@ +using GestionIntegral.Api.Dtos.Contables; +using GestionIntegral.Api.Services.Contables; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Contables +{ + [Route("api/saldos")] + [ApiController] + [Authorize] // Requiere autenticación para todos los endpoints + public class SaldosController : ControllerBase + { + private readonly ISaldoService _saldoService; + private readonly ILogger _logger; + + // Define un permiso específico para ver saldos, y otro para ajustarlos (SuperAdmin implícito) + private const string PermisoVerSaldos = "CS001"; // Ejemplo: Cuentas Saldos Ver + private const string PermisoAjustarSaldos = "CS002"; // Ejemplo: Cuentas Saldos Ajustar (o solo SuperAdmin) + + + public SaldosController(ISaldoService saldoService, ILogger logger) + { + _saldoService = saldoService; + _logger = logger; + } + + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en SaldosController."); + return null; + } + + // GET: api/saldos + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetSaldosGestion( + [FromQuery] string? destino, + [FromQuery] int? idDestino, + [FromQuery] int? idEmpresa) + { + if (!TienePermiso(PermisoVerSaldos)) // Usar el nuevo permiso + { + _logger.LogWarning("Acceso denegado a GetSaldosGestion para Usuario ID {userId}", GetCurrentUserId() ?? 0); + return Forbid(); + } + + try + { + var saldos = await _saldoService.ObtenerSaldosParaGestionAsync(destino, idDestino, idEmpresa); + return Ok(saldos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener saldos para gestión."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener saldos."); + } + } + + // POST: api/saldos/ajustar + [HttpPost("ajustar")] + [ProducesResponseType(typeof(SaldoGestionDto), StatusCodes.Status200OK)] // Devuelve el saldo actualizado + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] // Solo SuperAdmin o con permiso específico + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task AjustarSaldoManualmente([FromBody] AjusteSaldoRequestDto ajusteDto) + { + // Esta operación debería ser MUY restringida. Solo SuperAdmin o un permiso muy específico. + if (!User.IsInRole("SuperAdmin") && !TienePermiso(PermisoAjustarSaldos)) + { + _logger.LogWarning("Intento no autorizado de ajustar saldo por Usuario ID {userId}", GetCurrentUserId() ?? 0); + return Forbid("No tiene permisos para realizar ajustes manuales de saldo."); + } + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo identificar al usuario."); + + try + { + var (exito, error, saldoActualizado) = await _saldoService.RealizarAjusteManualSaldoAsync(ajusteDto, idUsuario.Value); + if (!exito) + { + if (error != null && error.Contains("No se encontró un saldo existente")) + return NotFound(new { message = error }); + return BadRequest(new { message = error ?? "Error desconocido al ajustar el saldo." }); + } + return Ok(saldoActualizado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error crítico al ajustar saldo manualmente."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar el ajuste de saldo."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs index b233884..bae03d9 100644 --- a/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs @@ -47,11 +47,11 @@ namespace GestionIntegral.Api.Controllers.Distribucion [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? soloActivos = true) + public async Task GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true) { if (!TienePermiso(PermisoVer)) return Forbid(); - var canillas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos); - return Ok(canillas); + var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); // <<-- Pasa el parámetro + return Ok(canillitas); } // GET: api/canillas/{id} @@ -117,7 +117,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion public async Task ToggleBajaCanilla(int id, [FromBody] ToggleBajaCanillaDto bajaDto) { if (!TienePermiso(PermisoBaja)) return Forbid(); - if (!ModelState.IsValid) return BadRequest(ModelState); + if (!ModelState.IsValid) return BadRequest(ModelState); var idUsuario = GetCurrentUserId(); if (idUsuario == null) return Unauthorized(); diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs index 910684e..c1219a2 100644 --- a/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs @@ -47,6 +47,15 @@ namespace GestionIntegral.Api.Controllers.Distribucion return Ok(distribuidores); } + [HttpGet("dropdown")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllDropdownDistribuidores() + { + var distribuidores = await _distribuidorService.GetAllDropdownAsync(); + return Ok(distribuidores); + } + [HttpGet("{id:int}", Name = "GetDistribuidorById")] [ProducesResponseType(typeof(DistribuidorDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -59,6 +68,17 @@ namespace GestionIntegral.Api.Controllers.Distribucion return Ok(distribuidor); } + [HttpGet("{id:int}/lookup", Name = "GetDistribuidorLookupById")] + [ProducesResponseType(typeof(DistribuidorLookupDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ObtenerLookupPorIdAsync(int id) + { + var distribuidor = await _distribuidorService.ObtenerLookupPorIdAsync(id); + if (distribuidor == null) return NotFound(); + return Ok(distribuidor); + } + [HttpPost] [ProducesResponseType(typeof(DistribuidorDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs index 0c3de6b..edd4eb5 100644 --- a/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs @@ -74,6 +74,25 @@ namespace GestionIntegral.Api.Controllers // Ajusta el namespace si es necesario } } + // GET: api/empresas/dropdown + [HttpGet("dropdown")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEmpresasDropdown() + { + try + { + var empresas = await _empresaService.ObtenerParaDropdown(); + return Ok(empresas); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Empresas."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener las empresas."); + } + } + // GET: api/empresas/{id} // Permiso Requerido: DE001 (Ver Empresas) [HttpGet("{id:int}", Name = "GetEmpresaById")] @@ -101,6 +120,29 @@ namespace GestionIntegral.Api.Controllers // Ajusta el namespace si es necesario } } + [HttpGet("{id:int}/lookup", Name = "GetEmpresaLookupById")] + [ProducesResponseType(typeof(EmpresaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task ObtenerLookupPorIdAsync(int id) + { + try + { + var empresa = await _empresaService.ObtenerLookupPorIdAsync(id); + if (empresa == null) + { + return NotFound(new { message = $"Empresa con ID {id} no encontrada." }); + } + return Ok(empresa); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Empresa por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la empresa."); + } + } + // POST: api/empresas // Permiso Requerido: DE002 (Agregar Empresas) [HttpPost] diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/NovedadesCanillaController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/NovedadesCanillaController.cs new file mode 100644 index 0000000..2305fe7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/NovedadesCanillaController.cs @@ -0,0 +1,212 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/novedadescanilla")] // Ruta base más genérica para las novedades + [ApiController] + [Authorize] // Todas las acciones requieren autenticación + public class NovedadesCanillaController : ControllerBase + { + private readonly INovedadCanillaService _novedadService; + private readonly ILogger _logger; + + public NovedadesCanillaController(INovedadCanillaService novedadService, ILogger logger) + { + _novedadService = novedadService; + _logger = logger; + } + + // --- Helper para verificar permisos --- + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + // --- Helper para obtener User ID --- + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) + { + return userId; + } + _logger.LogWarning("No se pudo obtener el UserId del token JWT en NovedadesCanillaController."); + return null; + } + + // GET: api/novedadescanilla/porcanilla/{idCanilla} + // Obtiene todas las novedades para un canillita específico, opcionalmente filtrado por fecha. + // Permiso: CG001 (Ver Canillas) o CG006 (Gestionar Novedades). + // Si CG006 es "Permite la Carga/Modificación", entonces CG001 podría ser más apropiado solo para ver. + // Vamos a usar CG001 para ver. Si se quiere más granularidad, se puede crear un permiso "Ver Novedades". + [HttpGet("porcanilla/{idCanilla:int}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetNovedadesPorCanilla(int idCanilla, [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta) + { + if (!TienePermiso("CG001") && !TienePermiso("CG006")) // Necesita al menos uno de los dos + { + _logger.LogWarning("Acceso denegado a GetNovedadesPorCanilla para el usuario {UserId} y canillita {IdCanilla}", GetCurrentUserId() ?? 0, idCanilla); + return Forbid(); + } + + try + { + var novedades = await _novedadService.ObtenerPorCanillaAsync(idCanilla, fechaDesde, fechaHasta); + return Ok(novedades); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener novedades para Canillita ID: {IdCanilla}", idCanilla); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener las novedades."); + } + } + + // GET: api/novedadescanilla/{idNovedad} + // Obtiene una novedad específica por su ID. + // Permiso: CG001 o CG006 + [HttpGet("{idNovedad:int}", Name = "GetNovedadCanillaById")] + [ProducesResponseType(typeof(NovedadCanillaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetNovedadCanillaById(int idNovedad) + { + if (!TienePermiso("CG001") && !TienePermiso("CG006")) return Forbid(); + + try + { + var novedad = await _novedadService.ObtenerPorIdAsync(idNovedad); + if (novedad == null) + { + return NotFound(new { message = $"Novedad con ID {idNovedad} no encontrada." }); + } + return Ok(novedad); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener NovedadCanilla por ID: {IdNovedad}", idNovedad); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la novedad."); + } + } + + + // POST: api/novedadescanilla + // Crea una nueva novedad. El IdCanilla viene en el DTO. + // Permiso: CG006 (Permite la Carga/Modificación de Novedades) + [HttpPost] + [ProducesResponseType(typeof(NovedadCanillaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateNovedadCanilla([FromBody] CreateNovedadCanillaDto createDto) + { + if (!TienePermiso("CG006")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (novedadCreada, error) = await _novedadService.CrearAsync(createDto, idUsuario.Value); + + if (error != null) return BadRequest(new { message = error }); + if (novedadCreada == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la novedad."); + + // Devuelve la ruta al recurso creado y el recurso mismo + return CreatedAtRoute("GetNovedadCanillaById", new { idNovedad = novedadCreada.IdNovedad }, novedadCreada); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear NovedadCanilla para Canilla ID: {IdCanilla} por Usuario ID: {UsuarioId}", createDto.IdCanilla, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al crear la novedad."); + } + } + + // PUT: api/novedadescanilla/{idNovedad} + // Actualiza una novedad existente. + // Permiso: CG006 + [HttpPut("{idNovedad:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdateNovedadCanilla(int idNovedad, [FromBody] UpdateNovedadCanillaDto updateDto) + { + if (!TienePermiso("CG006")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _novedadService.ActualizarAsync(idNovedad, updateDto, idUsuario.Value); + + if (!exito) + { + if (error == "Novedad no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar NovedadCanilla ID: {IdNovedad} por Usuario ID: {UsuarioId}", idNovedad, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar la novedad."); + } + } + + // DELETE: api/novedadescanilla/{idNovedad} + // Elimina una novedad. + // Permiso: CG006 (Asumiendo que el mismo permiso para Carga/Modificación incluye eliminación) + // Si la eliminación es un permiso separado (ej: CG00X), ajústalo. + [HttpDelete("{idNovedad:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteNovedadCanilla(int idNovedad) + { + if (!TienePermiso("CG006")) return Forbid(); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _novedadService.EliminarAsync(idNovedad, idUsuario.Value); + + if (!exito) + { + if (error == "Novedad no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Podría ser otro error, como "no se pudo eliminar" + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar NovedadCanilla ID: {IdNovedad} por Usuario ID: {UsuarioId}", idNovedad, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al eliminar la novedad."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs index 7ad59a4..e3033bc 100644 --- a/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Reportes/ReportesController.cs @@ -11,6 +11,7 @@ using GestionIntegral.Api.Data.Repositories.Impresion; using System.IO; using System.Linq; using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Services.Distribucion; namespace GestionIntegral.Api.Controllers { @@ -25,6 +26,7 @@ namespace GestionIntegral.Api.Controllers private readonly IPublicacionRepository _publicacionRepository; private readonly IEmpresaRepository _empresaRepository; private readonly IDistribuidorRepository _distribuidorRepository; // Para obtener el nombre del distribuidor + private readonly INovedadCanillaService _novedadCanillaService; // Permisos @@ -36,22 +38,25 @@ namespace GestionIntegral.Api.Controllers private const string PermisoVerBalanceCuentas = "RR001"; private const string PermisoVerReporteTiradas = "RR008"; private const string PermisoVerReporteConsumoBobinas = "RR007"; - + private const string PermisoVerReporteNovedadesCanillas = "RR004"; + private const string PermisoVerReporteListadoDistMensual = "RR009"; public ReportesController( - IReportesService reportesService, // <--- CORREGIDO + IReportesService reportesService, + INovedadCanillaService novedadCanillaService, ILogger logger, IPlantaRepository plantaRepository, IPublicacionRepository publicacionRepository, IEmpresaRepository empresaRepository, - IDistribuidorRepository distribuidorRepository) // Añadido + IDistribuidorRepository distribuidorRepository) { - _reportesService = reportesService; // <--- CORREGIDO + _reportesService = reportesService; + _novedadCanillaService = novedadCanillaService; _logger = logger; _plantaRepository = plantaRepository; _publicacionRepository = publicacionRepository; _empresaRepository = empresaRepository; - _distribuidorRepository = distribuidorRepository; // Añadido + _distribuidorRepository = distribuidorRepository; } private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); @@ -1457,5 +1462,277 @@ namespace GestionIntegral.Api.Controllers return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del ticket."); } } + + // GET: api/reportes/novedades-canillas + // Obtiene los datos para el reporte de novedades de canillitas + [HttpGet("novedades-canillas")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] // Si no hay datos + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetReporteNovedadesCanillasData( + [FromQuery] int idEmpresa, + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta) + { + if (!TienePermiso(PermisoVerReporteNovedadesCanillas)) + { + _logger.LogWarning("Acceso denegado a GetReporteNovedadesCanillasData. Usuario: {User}", User.Identity?.Name ?? "Desconocido"); + return Forbid(); + } + + if (fechaDesde > fechaHasta) + { + return BadRequest(new { message = "La fecha 'desde' no puede ser posterior a la fecha 'hasta'." }); + } + + try + { + var reporteData = await _novedadCanillaService.ObtenerReporteNovedadesAsync(idEmpresa, fechaDesde, fechaHasta); + if (reporteData == null || !reporteData.Any()) + { + // Devolver Ok con array vacío en lugar de NotFound para que el frontend pueda manejarlo como "sin datos" + return Ok(Enumerable.Empty()); + } + return Ok(reporteData); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al generar datos para el reporte de novedades de canillitas. Empresa: {IdEmpresa}, Desde: {FechaDesde}, Hasta: {FechaHasta}", idEmpresa, fechaDesde, fechaHasta); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Error interno al generar el reporte de novedades." }); + } + } + + // GET: api/reportes/novedades-canillas/pdf + // Genera el PDF del reporte de novedades de canillitas + [HttpGet("novedades-canillas/pdf")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetReporteNovedadesCanillasPdf( + [FromQuery] int idEmpresa, + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta) + { + if (!TienePermiso(PermisoVerReporteNovedadesCanillas)) // RR004 + { + _logger.LogWarning("Acceso denegado a GetReporteNovedadesCanillasPdf. Usuario: {User}", User.Identity?.Name ?? "Desconocido"); + return Forbid(); + } + if (fechaDesde > fechaHasta) + { + return BadRequest(new { message = "La fecha 'desde' no puede ser posterior a la fecha 'hasta'." }); + } + + try + { + // Obtener datos para AMBOS datasets + var novedadesData = await _novedadCanillaService.ObtenerReporteNovedadesAsync(idEmpresa, fechaDesde, fechaHasta); + var gananciasData = await _novedadCanillaService.ObtenerReporteGananciasAsync(idEmpresa, fechaDesde, fechaHasta); // << OBTENER DATOS DE GANANCIAS + + // Verificar si hay datos en *alguno* de los datasets necesarios para el reporte + if ((novedadesData == null || !novedadesData.Any()) && (gananciasData == null || !gananciasData.Any())) + { + return NotFound(new { message = "No hay datos para generar el PDF con los parámetros seleccionados." }); + } + + var empresa = await _empresaRepository.GetByIdAsync(idEmpresa); + + LocalReport report = new LocalReport(); + string rdlcPath = Path.Combine("Controllers", "Reportes", "RDLC", "ReporteListadoNovedadesCanillas.rdlc"); + if (!System.IO.File.Exists(rdlcPath)) + { + _logger.LogError("Archivo RDLC no encontrado en la ruta: {RdlcPath}", rdlcPath); + return StatusCode(StatusCodes.Status500InternalServerError, "Archivo de definición de reporte no encontrado."); + } + + using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + report.LoadReportDefinition(fs); + } + + // Nombre del DataSet en RDLC para SP_DistCanillasNovedades (detalles) + report.DataSources.Add(new ReportDataSource("DSNovedadesCanillasDetalles", novedadesData ?? new List())); + + // Nombre del DataSet en RDLC para SP_DistCanillasGanancias (ganancias/resumen) + report.DataSources.Add(new ReportDataSource("DSNovedadesCanillas", gananciasData ?? new List())); + + + var parameters = new List + { + new ReportParameter("NomEmp", empresa?.Nombre ?? "N/A"), + 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 = $"ReporteNovedadesCanillas_Emp{idEmpresa}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf"; + return File(pdfBytes, "application/pdf", fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al generar PDF para el reporte de novedades de canillitas. Empresa: {IdEmpresa}", idEmpresa); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = $"Error interno al generar el PDF: {ex.Message}" }); + } + } + // GET: api/reportes/novedades-canillas-ganancias + [HttpGet("novedades-canillas-ganancias")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetReporteGananciasCanillasData( + [FromQuery] int idEmpresa, + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta) + { + if (!TienePermiso(PermisoVerReporteNovedadesCanillas)) return Forbid(); // RR004 + + if (fechaDesde > fechaHasta) + { + return BadRequest(new { message = "La fecha 'desde' no puede ser posterior a la fecha 'hasta'." }); + } + try + { + var gananciasData = await _novedadCanillaService.ObtenerReporteGananciasAsync(idEmpresa, fechaDesde, fechaHasta); + if (gananciasData == null || !gananciasData.Any()) + { + return Ok(Enumerable.Empty()); + } + return Ok(gananciasData); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener datos de ganancias para el reporte de novedades. Empresa: {IdEmpresa}", idEmpresa); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Error interno al obtener datos de ganancias." }); + } + } + + // GET: api/reportes/listado-distribucion-mensual/diarios + [HttpGet("listado-distribucion-mensual/diarios")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetListadoDistMensualDiarios( + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta, + [FromQuery] bool esAccionista) + { + if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid(); + if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." }); + + var (data, error) = await _reportesService.ObtenerReporteMensualDiariosAsync(fechaDesde, fechaHasta, esAccionista); + if (error != null) return BadRequest(new { message = error }); + return Ok(data ?? Enumerable.Empty()); + } + + [HttpGet("listado-distribucion-mensual/diarios/pdf")] + public async Task GetListadoDistMensualDiariosPdf( + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta, + [FromQuery] bool esAccionista) + { + if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid(); + if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." }); + + var (data, error) = await _reportesService.ObtenerReporteMensualDiariosAsync(fechaDesde, fechaHasta, esAccionista); + if (error != null) return BadRequest(new { message = error }); + if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el PDF." }); + + try + { + LocalReport report = new LocalReport(); + string rdlcPath = Path.Combine("Controllers", "Reportes", "RDLC", "ReporteListadoDistribucionCanMensualDiarios.rdlc"); + if (!System.IO.File.Exists(rdlcPath)) + { + _logger.LogError("Archivo RDLC no encontrado: {Path}", rdlcPath); + return StatusCode(StatusCodes.Status500InternalServerError, $"Archivo de reporte no encontrado: {Path.GetFileName(rdlcPath)}"); + } + using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + report.LoadReportDefinition(fs); + } + report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCanMensualDiarios", data)); + + var parameters = new List + { + new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")), + new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")), + new ReportParameter("CanAcc", esAccionista ? "1" : "0") // El RDLC espera un Integer para CanAcc + }; + report.SetParameters(parameters); + + byte[] pdfBytes = report.Render("PDF"); + string tipoDesc = esAccionista ? "Accionistas" : "Canillitas"; + return File(pdfBytes, "application/pdf", $"ListadoDistMensualDiarios_{tipoDesc}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf"); + } + catch (Exception ex) { _logger.LogError(ex, "Error PDF ListadoDistMensualDiarios"); return StatusCode(500, "Error interno."); } + } + + + // GET: api/reportes/listado-distribucion-mensual/publicaciones + [HttpGet("listado-distribucion-mensual/publicaciones")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetListadoDistMensualPorPublicacion( + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta, + [FromQuery] bool esAccionista) + { + if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid(); + if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." }); + + var (data, error) = await _reportesService.ObtenerReporteMensualPorPublicacionAsync(fechaDesde, fechaHasta, esAccionista); + if (error != null) return BadRequest(new { message = error }); + return Ok(data ?? Enumerable.Empty()); + } + + [HttpGet("listado-distribucion-mensual/publicaciones/pdf")] + public async Task GetListadoDistMensualPorPublicacionPdf( + [FromQuery] DateTime fechaDesde, + [FromQuery] DateTime fechaHasta, + [FromQuery] bool esAccionista) + { + if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid(); + if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." }); + + var (data, error) = await _reportesService.ObtenerReporteMensualPorPublicacionAsync(fechaDesde, fechaHasta, esAccionista); + if (error != null) return BadRequest(new { message = error }); + if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el PDF." }); + + try + { + LocalReport report = new LocalReport(); + string rdlcPath = Path.Combine("Controllers", "Reportes", "RDLC", "ReporteListadoDistribucionCanMensual.rdlc"); + if (!System.IO.File.Exists(rdlcPath)) + { + _logger.LogError("Archivo RDLC no encontrado: {Path}", rdlcPath); + return StatusCode(StatusCodes.Status500InternalServerError, $"Archivo de reporte no encontrado: {Path.GetFileName(rdlcPath)}"); + } + using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + report.LoadReportDefinition(fs); + } + report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCanMensual", data)); + + var parameters = new List + { + new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")), + new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")), + new ReportParameter("CanAcc", esAccionista ? "1" : "0") + }; + report.SetParameters(parameters); + + byte[] pdfBytes = report.Render("PDF"); + string tipoDesc = esAccionista ? "Accionistas" : "Canillitas"; + return File(pdfBytes, "application/pdf", $"ListadoDistMensualPub_{tipoDesc}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf"); + } + catch (Exception ex) { _logger.LogError(ex, "Error PDF ListadoDistMensualPorPublicacion"); return StatusCode(500, "Error interno."); } + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs index 806a6b7..6afe7af 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using System.Collections.Generic; // Para IEnumerable using System.Data; +using GestionIntegral.Api.Dtos.Contables; // Para SaldoGestionDto si lo usas aquí +using GestionIntegral.Api.Models.Contables; // Para Saldo, SaldoAjusteHistorial namespace GestionIntegral.Api.Data.Repositories.Contables { @@ -15,5 +17,12 @@ namespace GestionIntegral.Api.Data.Repositories.Contables // Método para modificar saldo (lo teníamos como privado antes, ahora en el repo) Task ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null); Task CheckIfSaldosExistForEmpresaAsync(int id); + + // Para obtener la lista de saldos para la página de gestión + Task> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter); + // Para obtener un saldo específico (ya podría existir uno similar, o crearlo si es necesario) + Task GetSaldoAsync(string destino, int idDestino, int idEmpresa, IDbTransaction? transaction = null); + // Para registrar el historial de ajuste + Task CreateSaldoAjusteHistorialAsync(SaldoAjusteHistorial historialEntry, IDbTransaction transaction); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs index 308ff6f..ec5fa88 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs @@ -1,10 +1,12 @@ using Dapper; -using GestionIntegral.Api.Data.Repositories; +using GestionIntegral.Api.Data.Repositories; using GestionIntegral.Api.Models; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data; using System.Threading.Tasks; +using GestionIntegral.Api.Models.Contables; +using System.Text; namespace GestionIntegral.Api.Data.Repositories.Contables { @@ -57,61 +59,64 @@ namespace GestionIntegral.Api.Data.Repositories.Contables public async Task DeleteSaldosByEmpresaAsync(int idEmpresa, IDbTransaction transaction) { - var sql = "DELETE FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa"; - try - { - await transaction.Connection!.ExecuteAsync(sql, new { IdEmpresa = idEmpresa }, transaction: transaction); - return true; // Asumir éxito si no hay excepción - } - catch (Exception ex) - { - _logger.LogError(ex, "Error al eliminar saldos para Empresa ID {IdEmpresa}.", idEmpresa); - throw; - } + var sql = "DELETE FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa"; + try + { + await transaction.Connection!.ExecuteAsync(sql, new { IdEmpresa = idEmpresa }, transaction: transaction); + return true; // Asumir éxito si no hay excepción + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar saldos para Empresa ID {IdEmpresa}.", idEmpresa); + throw; + } } public async Task ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null) - { - var sql = @"UPDATE dbo.cue_Saldos - SET Monto = Monto + @MontoAAgregar - WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;"; + { + var sql = @"UPDATE dbo.cue_Saldos + SET Monto = Monto + @MontoAAgregar, + FechaUltimaModificacion = @FechaActualizacion -- << AÑADIR + WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;"; - // Usar una variable para la conexión para poder aplicar el '!' si es necesario - IDbConnection connection = transaction?.Connection ?? _connectionFactory.CreateConnection(); - bool ownConnection = transaction == null; // Saber si necesitamos cerrar la conexión nosotros + IDbConnection connection = transaction?.Connection ?? _connectionFactory.CreateConnection(); + bool ownConnection = transaction == null; - try - { - if (ownConnection) await (connection as System.Data.Common.DbConnection)!.OpenAsync(); // Abrir solo si no hay transacción externa + try + { + if (ownConnection && connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } - var parameters = new { - MontoAAgregar = montoAAgregar, - Destino = destino, - IdDestino = idDestino, - IdEmpresa = idEmpresa - }; - // Aplicar '!' aquí también si viene de la transacción - int rowsAffected = await connection.ExecuteAsync(sql, parameters, transaction: transaction); - return rowsAffected == 1; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error al modificar saldo para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa}.", destino, idDestino, idEmpresa); - if (transaction != null) throw; // Re-lanzar si estamos en una transacción externa - return false; // Devolver false si fue una operación aislada que falló - } - finally - { - // Cerrar la conexión solo si la abrimos nosotros (no había transacción externa) - if (ownConnection && connection.State == ConnectionState.Open) - { - await (connection as System.Data.Common.DbConnection)!.CloseAsync(); - } - // Disponer de la conexión si la creamos nosotros - if(ownConnection) (connection as IDisposable)?.Dispose(); - } - } - public async Task CheckIfSaldosExistForEmpresaAsync(int idEmpresa) + var parameters = new + { + MontoAAgregar = montoAAgregar, + Destino = destino, + IdDestino = idDestino, + IdEmpresa = idEmpresa, + FechaActualizacion = DateTime.Now // O DateTime.UtcNow si prefieres + }; + int rowsAffected = await connection.ExecuteAsync(sql, parameters, transaction: transaction); + return rowsAffected == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al modificar saldo para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa}.", destino, idDestino, idEmpresa); + if (transaction != null) throw; + return false; + } + finally + { + if (ownConnection && connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + if (ownConnection && connection is IDisposable d) d.Dispose(); // Mejorar dispose + } + } + + public async Task CheckIfSaldosExistForEmpresaAsync(int idEmpresa) { var sql = "SELECT COUNT(1) FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa"; try @@ -130,5 +135,58 @@ namespace GestionIntegral.Api.Data.Repositories.Contables // O podrías devolver true para ser más conservador si la verificación es crítica. } } + + public async Task> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion FROM dbo.cue_Saldos WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(destinoFilter)) + { + sqlBuilder.Append(" AND Destino = @Destino"); + parameters.Add("Destino", destinoFilter); + } + if (idDestinoFilter.HasValue) + { + sqlBuilder.Append(" AND Id_Destino = @IdDestino"); + parameters.Add("IdDestino", idDestinoFilter.Value); + } + if (idEmpresaFilter.HasValue) + { + sqlBuilder.Append(" AND Id_Empresa = @IdEmpresa"); + parameters.Add("IdEmpresa", idEmpresaFilter.Value); + } + sqlBuilder.Append(" ORDER BY Destino, Id_Empresa, Id_Destino;"); + + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + + public async Task GetSaldoAsync(string destino, int idDestino, int idEmpresa, IDbTransaction? transaction = null) + { + const string sql = "SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion FROM dbo.cue_Saldos WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;"; + var conn = transaction?.Connection ?? _connectionFactory.CreateConnection(); + if (transaction == null && conn.State != ConnectionState.Open) { if (conn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else conn.Open(); } + + try + { + return await conn.QuerySingleOrDefaultAsync(sql, new { Destino = destino, IdDestino = idDestino, IdEmpresa = idEmpresa }, transaction); + } + finally + { + if (transaction == null && conn.State == ConnectionState.Open) { if (conn is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else conn.Close(); } + } + } + + public async Task CreateSaldoAjusteHistorialAsync(SaldoAjusteHistorial historialEntry, IDbTransaction transaction) + { + const string sql = @" + INSERT INTO dbo.cue_SaldoAjustesHistorial + (Destino, Id_Destino, Id_Empresa, MontoAjuste, SaldoAnterior, SaldoNuevo, Justificacion, FechaAjuste, Id_UsuarioAjuste) + VALUES + (@Destino, @IdDestino, @IdEmpresa, @MontoAjuste, @SaldoAnterior, @SaldoNuevo, @Justificacion, @FechaAjuste, @IdUsuarioAjuste);"; + + await transaction.Connection!.ExecuteAsync(sql, historialEntry, transaction); + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs index 8486f78..9691872 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs @@ -21,51 +21,56 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion _logger = logger; } - public async Task> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos) + public async Task> GetAllAsync( + string? nomApeFilter, + int? legajoFilter, + bool? esAccionista, + bool? soloActivos) // <<-- Parámetro aquí { - var sqlBuilder = new StringBuilder(@" - SELECT - c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe, c.Parada, c.Id_Zona AS IdZona, - c.Accionista, c.Obs, c.Empresa, c.Baja, c.FechaBaja, - z.Nombre AS NombreZona, - ISNULL(e.Nombre, 'N/A (Accionista)') AS NombreEmpresa - FROM dbo.dist_dtCanillas c - INNER JOIN dbo.dist_dtZonas z ON c.Id_Zona = z.Id_Zona - LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa - WHERE 1=1"); + using var connection = _connectionFactory.CreateConnection(); + var sqlBuilder = new System.Text.StringBuilder(@" + SELECT c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe, c.Parada, c.Id_Zona AS IdZona, + c.Accionista, c.Obs, c.Empresa, c.Baja, c.FechaBaja, + z.Nombre AS NombreZona, + e.Nombre AS NombreEmpresa + FROM dbo.dist_dtCanillas c + LEFT JOIN dbo.dist_dtZonas z ON c.Id_Zona = z.Id_Zona + LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa + WHERE 1=1 "); // Cláusula base para añadir AND fácilmente + var parameters = new DynamicParameters(); - if (soloActivos.HasValue) - { - sqlBuilder.Append(soloActivos.Value ? " AND c.Baja = 0" : " AND c.Baja = 1"); - } if (!string.IsNullOrWhiteSpace(nomApeFilter)) { - sqlBuilder.Append(" AND c.NomApe LIKE @NomApeParam"); - parameters.Add("NomApeParam", $"%{nomApeFilter}%"); + sqlBuilder.Append(" AND c.NomApe LIKE @NomApeFilter "); + parameters.Add("NomApeFilter", $"%{nomApeFilter}%"); } if (legajoFilter.HasValue) { - sqlBuilder.Append(" AND c.Legajo = @LegajoParam"); - parameters.Add("LegajoParam", legajoFilter.Value); + sqlBuilder.Append(" AND c.Legajo = @LegajoFilter "); + parameters.Add("LegajoFilter", legajoFilter.Value); } + if (soloActivos.HasValue) + { + sqlBuilder.Append(" AND c.Baja = @BajaStatus "); + parameters.Add("BajaStatus", !soloActivos.Value); // Si soloActivos es true, Baja debe ser false + } + + if (esAccionista.HasValue) + { + sqlBuilder.Append(" AND c.Accionista = @EsAccionista "); + parameters.Add("EsAccionista", esAccionista.Value); // true para accionistas, false para no accionistas (canillitas) + } + sqlBuilder.Append(" ORDER BY c.NomApe;"); - try - { - using var connection = _connectionFactory.CreateConnection(); - return await connection.QueryAsync( - sqlBuilder.ToString(), - (canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa), - parameters, - splitOn: "NombreZona,NombreEmpresa" - ); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error al obtener todos los Canillas. Filtros: NomApe='{NomApeFilter}', Legajo='{LegajoFilter}', SoloActivos='{SoloActivos}'", nomApeFilter, legajoFilter, soloActivos); - return Enumerable.Empty<(Canilla, string, string)>(); - } + var result = await connection.QueryAsync( + sqlBuilder.ToString(), + (can, zona, emp) => (can, zona, emp), + parameters, + splitOn: "NombreZona,NombreEmpresa" + ); + return result; } public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id) @@ -83,12 +88,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion try { using var connection = _connectionFactory.CreateConnection(); - var result = await connection.QueryAsync( - sql, - (canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa), - new { IdParam = id }, - splitOn: "NombreZona,NombreEmpresa" - ); + var result = await connection.QueryAsync( + sql, + (canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa), + new { IdParam = id }, + splitOn: "NombreZona,NombreEmpresa" + ); return result.SingleOrDefault(); } catch (Exception ex) @@ -160,9 +165,19 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion await connection.ExecuteAsync(sqlInsertHistorico, new { - IdCanillaParam = insertedCanilla.IdCanilla, LegajoParam = insertedCanilla.Legajo, NomApeParam = insertedCanilla.NomApe, ParadaParam = insertedCanilla.Parada, IdZonaParam = insertedCanilla.IdZona, - AccionistaParam = insertedCanilla.Accionista, ObsParam = insertedCanilla.Obs, EmpresaParam = insertedCanilla.Empresa, BajaParam = insertedCanilla.Baja, FechaBajaParam = insertedCanilla.FechaBaja, - Id_UsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Creado" + IdCanillaParam = insertedCanilla.IdCanilla, + LegajoParam = insertedCanilla.Legajo, + NomApeParam = insertedCanilla.NomApe, + ParadaParam = insertedCanilla.Parada, + IdZonaParam = insertedCanilla.IdZona, + AccionistaParam = insertedCanilla.Accionista, + ObsParam = insertedCanilla.Obs, + EmpresaParam = insertedCanilla.Empresa, + BajaParam = insertedCanilla.Baja, + FechaBajaParam = insertedCanilla.FechaBaja, + Id_UsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Creado" }, transaction); return insertedCanilla; } @@ -173,7 +188,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion var canillaActual = await connection.QuerySingleOrDefaultAsync( @"SELECT Id_Canilla AS IdCanilla, Legajo, NomApe, Parada, Id_Zona AS IdZona, Accionista, Obs, Empresa, Baja, FechaBaja - FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanillaParam", + FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanillaParam", new { IdCanillaParam = canillaAActualizar.IdCanilla }, transaction); if (canillaActual == null) throw new KeyNotFoundException("Canilla no encontrado para actualizar."); @@ -187,13 +202,21 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) VALUES (@IdCanillaParam, @LegajoParam, @NomApeParam, @ParadaParam, @IdZonaParam, @AccionistaParam, @ObsParam, @EmpresaParam, @BajaParam, @FechaBajaParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);"; - await connection.ExecuteAsync(sqlInsertHistorico, new + await connection.ExecuteAsync(sqlInsertHistorico, new { IdCanillaParam = canillaActual.IdCanilla, - LegajoParam = canillaActual.Legajo, NomApeParam = canillaActual.NomApe, ParadaParam = canillaActual.Parada, IdZonaParam = canillaActual.IdZona, - AccionistaParam = canillaActual.Accionista, ObsParam = canillaActual.Obs, EmpresaParam = canillaActual.Empresa, - BajaParam = canillaActual.Baja, FechaBajaParam = canillaActual.FechaBaja, - Id_UsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Actualizado" + LegajoParam = canillaActual.Legajo, + NomApeParam = canillaActual.NomApe, + ParadaParam = canillaActual.Parada, + IdZonaParam = canillaActual.IdZona, + AccionistaParam = canillaActual.Accionista, + ObsParam = canillaActual.Obs, + EmpresaParam = canillaActual.Empresa, + BajaParam = canillaActual.Baja, + FechaBajaParam = canillaActual.FechaBaja, + Id_UsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Actualizado" }, transaction); var rowsAffected = await connection.ExecuteAsync(sqlUpdate, canillaAActualizar, transaction); @@ -206,7 +229,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion var canillaActual = await connection.QuerySingleOrDefaultAsync( @"SELECT Id_Canilla AS IdCanilla, Legajo, NomApe, Parada, Id_Zona AS IdZona, Accionista, Obs, Empresa, Baja, FechaBaja - FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanillaParam", + FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanillaParam", new { IdCanillaParam = id }, transaction); if (canillaActual == null) throw new KeyNotFoundException("Canilla no encontrado para dar de baja/alta."); @@ -218,10 +241,19 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion await connection.ExecuteAsync(sqlInsertHistorico, new { - IdCanillaParam = canillaActual.IdCanilla, LegajoParam = canillaActual.Legajo, NomApeParam = canillaActual.NomApe, ParadaParam = canillaActual.Parada, IdZonaParam = canillaActual.IdZona, - AccionistaParam = canillaActual.Accionista, ObsParam = canillaActual.Obs, EmpresaParam = canillaActual.Empresa, - BajaNuevaParam = darDeBaja, FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null), - Id_UsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModHistParam = (darDeBaja ? "Baja" : "Alta") + IdCanillaParam = canillaActual.IdCanilla, + LegajoParam = canillaActual.Legajo, + NomApeParam = canillaActual.NomApe, + ParadaParam = canillaActual.Parada, + IdZonaParam = canillaActual.IdZona, + AccionistaParam = canillaActual.Accionista, + ObsParam = canillaActual.Obs, + EmpresaParam = canillaActual.Empresa, + BajaNuevaParam = darDeBaja, + FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null), + Id_UsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModHistParam = (darDeBaja ? "Baja" : "Alta") }, transaction); var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { BajaParam = darDeBaja, FechaBajaParam = (darDeBaja ? fechaBaja : null), IdCanillaParam = id }, transaction); diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs index 73e0dd6..a8f0c5e 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs @@ -1,4 +1,5 @@ using Dapper; +using GestionIntegral.Api.Dtos.Distribucion; using GestionIntegral.Api.Models.Distribucion; using Microsoft.Extensions.Logging; using System; // Añadido para Exception @@ -61,6 +62,30 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion return Enumerable.Empty<(Distribuidor, string?)>(); } } + + public async Task> GetAllDropdownAsync() + { + var sqlBuilder = new StringBuilder(@" + SELECT + Id_Distribuidor AS IdDistribuidor, Nombre + FROM dbo.dist_dtDistribuidores + WHERE 1=1"); + var parameters = new DynamicParameters(); + sqlBuilder.Append(" ORDER BY Nombre;"); + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync( + sqlBuilder.ToString(), + parameters + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Distribuidores."); + return Enumerable.Empty(); + } + } public async Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id) { @@ -90,6 +115,25 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } } + public async Task ObtenerLookupPorIdAsync(int id) + { + const string sql = @" + SELECT + Id_Distribuidor AS IdDistribuidor, Nombre + FROM dbo.dist_dtDistribuidores + WHERE Id_Distribuidor = @IdParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParam = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Distribuidor por ID: {IdDistribuidor}", id); + return null; + } + } + public async Task GetByIdSimpleAsync(int id) { const string sql = @" diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs index 616d655..7c67261 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs @@ -1,5 +1,6 @@ using Dapper; using GestionIntegral.Api.Data.Repositories; +using GestionIntegral.Api.Dtos.Empresas; using GestionIntegral.Api.Models.Distribucion; using System.Collections.Generic; using System.Data; @@ -52,6 +53,25 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } } + public async Task> GetAllDropdownAsync() + { + var sqlBuilder = new StringBuilder("SELECT Id_Empresa AS IdEmpresa, Nombre FROM dbo.dist_dtEmpresas WHERE 1=1"); + var parameters = new DynamicParameters(); + sqlBuilder.Append(" ORDER BY Nombre;"); + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Empresas."); + return Enumerable.Empty(); + } + } + public async Task GetByIdAsync(int id) { var sql = "SELECT Id_Empresa AS IdEmpresa, Nombre, Detalle FROM dbo.dist_dtEmpresas WHERE Id_Empresa = @Id"; @@ -69,6 +89,23 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } } + public async Task ObtenerLookupPorIdAsync(int id) + { + var sql = "SELECT Id_Empresa AS IdEmpresa, Nombre FROM dbo.dist_dtEmpresas WHERE Id_Empresa = @Id"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Empresa por ID: {IdEmpresa}", id); + return null; + } + } + public async Task ExistsByNameAsync(string nombre, int? excludeId = null) { var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtEmpresas WHERE Nombre = @Nombre"); @@ -144,7 +181,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } // Insertar en historial - await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { IdEmpresa = insertedEmpresa.IdEmpresa, insertedEmpresa.Nombre, insertedEmpresa.Detalle, @@ -172,7 +210,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion VALUES (@IdEmpresa, @NombreActual, @DetalleActual, @IdUsuario, @FechaMod, @TipoMod);"; // Insertar en historial (estado anterior) - await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { IdEmpresa = empresaActual.IdEmpresa, NombreActual = empresaActual.Nombre, DetalleActual = empresaActual.Detalle, @@ -182,7 +221,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion }, transaction: transaction); // Actualizar principal - var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new { + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new + { empresaAActualizar.Nombre, empresaAActualizar.Detalle, empresaAActualizar.IdEmpresa @@ -202,7 +242,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion VALUES (@IdEmpresa, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; // Insertar en historial (estado antes de borrar) - await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { IdEmpresa = empresaActual.IdEmpresa, empresaActual.Nombre, empresaActual.Detalle, diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs index 644320a..a933104 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs @@ -7,7 +7,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { public interface ICanillaRepository { - Task> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos); + Task> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista); Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id); Task GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla Task CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction); diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs index 4966582..c75e552 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs @@ -2,6 +2,7 @@ using GestionIntegral.Api.Models.Distribucion; using System.Collections.Generic; using System.Threading.Tasks; using System.Data; +using GestionIntegral.Api.Dtos.Distribucion; namespace GestionIntegral.Api.Data.Repositories.Distribucion { @@ -16,5 +17,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion Task ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null); Task ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null); Task IsInUseAsync(int id); // Verificar en dist_EntradasSalidas, cue_PagosDistribuidor, dist_PorcPago + Task> GetAllDropdownAsync(); + Task ObtenerLookupPorIdAsync(int id); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs index a06b2a6..1a95f33 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs @@ -1,7 +1,8 @@ using GestionIntegral.Api.Models.Distribucion; using System.Collections.Generic; using System.Threading.Tasks; -using System.Data; // Para IDbTransaction +using System.Data; +using GestionIntegral.Api.Dtos.Empresas; // Para IDbTransaction namespace GestionIntegral.Api.Data.Repositories.Distribucion { @@ -14,5 +15,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Necesita transacción Task ExistsByNameAsync(string nombre, int? excludeId = null); Task IsInUseAsync(int id); + Task> GetAllDropdownAsync(); + Task ObtenerLookupPorIdAsync(int id); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/INovedadCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/INovedadCanillaRepository.cs new file mode 100644 index 0000000..44991ea --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/INovedadCanillaRepository.cs @@ -0,0 +1,23 @@ +using GestionIntegral.Api.Dtos.Reportes; +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; +using System.Data; // Para IDbTransaction +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface INovedadCanillaRepository + { + // Para obtener novedades y el nombre del canillita + Task> GetByCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta); + Task GetByIdAsync(int idNovedad); + Task CreateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null); + Task UpdateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null); + Task DeleteAsync(int idNovedad, int idUsuario, IDbTransaction? transaction = null); + // Podrías añadir un método para verificar si existe una novedad para un canillita en una fecha específica si es necesario + Task ExistsByCanillaAndFechaAsync(int idCanilla, DateTime fecha, int? excludeIdNovedad = null); + Task> GetReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); + Task> GetReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/NovedadCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/NovedadCanillaRepository.cs new file mode 100644 index 0000000..ee20c67 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/NovedadCanillaRepository.cs @@ -0,0 +1,236 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Configuration; // Para IConfiguration +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using Microsoft.Data.SqlClient; // O el proveedor de tu BD +using System.Linq; +using System.Threading.Tasks; +using GestionIntegral.Api.Dtos.Reportes; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class NovedadCanillaRepository : INovedadCanillaRepository + { + private readonly DbConnectionFactory _connectionFactory; // Inyecta tu DbConnectionFactory + private readonly ILogger _logger; + + public NovedadCanillaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task LogHistorialAsync(NovedadCanilla novedadOriginal, int idUsuario, string tipoMod, IDbConnection connection, IDbTransaction? transaction) + { + var historial = new NovedadCanillaHistorial + { + IdNovedad = novedadOriginal.IdNovedad, + IdCanilla = novedadOriginal.IdCanilla, + Fecha = novedadOriginal.Fecha, + Detalle = novedadOriginal.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = tipoMod + }; + var sqlHistorial = @" + INSERT INTO dbo.dist_dtNovedadesCanillas_H + (Id_Novedad, Id_Canilla, Fecha, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES + (@IdNovedad, @IdCanilla, @Fecha, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + await connection.ExecuteAsync(sqlHistorial, historial, transaction); + } + + public async Task> GetByCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta) + { + using var connection = _connectionFactory.CreateConnection(); + var sqlBuilder = new System.Text.StringBuilder(@" + SELECT + n.Id_Novedad AS IdNovedad, + n.Id_Canilla AS IdCanilla, + n.Fecha, + n.Detalle, + c.NomApe AS NombreCanilla + FROM dbo.dist_dtNovedadesCanillas n + JOIN dbo.dist_dtCanillas c ON n.Id_Canilla = c.Id_Canilla + WHERE n.Id_Canilla = @IdCanilla"); + + var parameters = new DynamicParameters(); + parameters.Add("IdCanilla", idCanilla); + + if (fechaDesde.HasValue) + { + sqlBuilder.Append(" AND n.Fecha >= @FechaDesde"); + parameters.Add("FechaDesde", fechaDesde.Value.Date); // Solo fecha, sin hora + } + if (fechaHasta.HasValue) + { + sqlBuilder.Append(" AND n.Fecha <= @FechaHasta"); + // Para incluir todo el día de fechaHasta + parameters.Add("FechaHasta", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); + } + sqlBuilder.Append(" ORDER BY n.Fecha DESC, n.Id_Novedad DESC;"); + + var result = await connection.QueryAsync( + sqlBuilder.ToString(), + (novedad, nombreCanilla) => (novedad, nombreCanilla), + parameters, + splitOn: "NombreCanilla" + ); + return result; + } + + + public async Task GetByIdAsync(int idNovedad) + { + using var connection = _connectionFactory.CreateConnection(); + var sql = "SELECT Id_Novedad AS IdNovedad, Id_Canilla AS IdCanilla, Fecha, Detalle FROM dbo.dist_dtNovedadesCanillas WHERE Id_Novedad = @IdNovedad;"; + return await connection.QuerySingleOrDefaultAsync(sql, new { IdNovedad = idNovedad }); + } + + public async Task ExistsByCanillaAndFechaAsync(int idCanilla, DateTime fecha, int? excludeIdNovedad = null) + { + using var connection = _connectionFactory.CreateConnection(); + var sqlBuilder = new System.Text.StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtNovedadesCanillas WHERE Id_Canilla = @IdCanilla AND Fecha = @Fecha"); + var parameters = new DynamicParameters(); + parameters.Add("IdCanilla", idCanilla); + parameters.Add("Fecha", fecha.Date); // Comparar solo la fecha + + if (excludeIdNovedad.HasValue) + { + sqlBuilder.Append(" AND Id_Novedad != @ExcludeIdNovedad"); + parameters.Add("ExcludeIdNovedad", excludeIdNovedad.Value); + } + + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + + public async Task CreateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null) + { + var sql = @" + INSERT INTO dbo.dist_dtNovedadesCanillas (Id_Canilla, Fecha, Detalle) + VALUES (@IdCanilla, @Fecha, @Detalle); + SELECT CAST(SCOPE_IDENTITY() as int);"; + + IDbConnection conn = transaction?.Connection ?? _connectionFactory.CreateConnection(); + bool manageConnection = transaction == null; // Solo gestionar si no hay transacción externa + + try + { + if (manageConnection && conn.State != ConnectionState.Open) await (conn as SqlConnection)!.OpenAsync(); + + var newId = await conn.QuerySingleAsync(sql, novedad, transaction); + novedad.IdNovedad = newId; + await LogHistorialAsync(novedad, idUsuario, "Insertada", conn, transaction); + return novedad; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear NovedadCanilla para Canilla ID: {IdCanilla}", novedad.IdCanilla); + return null; + } + finally + { + if (manageConnection && conn.State == ConnectionState.Open) conn.Close(); + } + } + + public async Task UpdateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null) + { + var novedadOriginal = await GetByIdAsync(novedad.IdNovedad); // Necesitamos el estado original para el log + if (novedadOriginal == null) return false; // No se encontró + + var sql = @" + UPDATE dbo.dist_dtNovedadesCanillas SET + Detalle = @Detalle + -- No se permite cambiar IdCanilla ni Fecha de una novedad existente + WHERE Id_Novedad = @IdNovedad;"; + + IDbConnection conn = transaction?.Connection ?? _connectionFactory.CreateConnection(); + bool manageConnection = transaction == null; + + try + { + if (manageConnection && conn.State != ConnectionState.Open) await (conn as SqlConnection)!.OpenAsync(); + + await LogHistorialAsync(novedadOriginal, idUsuario, "Modificada", conn, transaction); // Log con datos ANTES de actualizar + var affectedRows = await conn.ExecuteAsync(sql, novedad, transaction); + return affectedRows > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar NovedadCanilla ID: {IdNovedad}", novedad.IdNovedad); + return false; + } + finally + { + if (manageConnection && conn.State == ConnectionState.Open) conn.Close(); + } + } + + public async Task DeleteAsync(int idNovedad, int idUsuario, IDbTransaction? transaction = null) + { + var novedadOriginal = await GetByIdAsync(idNovedad); + if (novedadOriginal == null) return false; + + var sql = "DELETE FROM dbo.dist_dtNovedadesCanillas WHERE Id_Novedad = @IdNovedad;"; + + IDbConnection conn = transaction?.Connection ?? _connectionFactory.CreateConnection(); + bool manageConnection = transaction == null; + + try + { + if (manageConnection && conn.State != ConnectionState.Open) await (conn as SqlConnection)!.OpenAsync(); + + await LogHistorialAsync(novedadOriginal, idUsuario, "Eliminada", conn, transaction); + var affectedRows = await conn.ExecuteAsync(sql, new { IdNovedad = idNovedad }, transaction); + return affectedRows > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar NovedadCanilla ID: {IdNovedad}", idNovedad); + return false; + } + finally + { + if (manageConnection && conn.State == ConnectionState.Open) conn.Close(); + } + } + + public async Task> GetReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta) + { + using var connection = _connectionFactory.CreateConnection(); + var parameters = new + { + idEmpresa, + fechaDesde = fechaDesde.Date, // Enviar solo la fecha + fechaHasta = fechaHasta.Date.AddDays(1).AddTicks(-1) // Para incluir todo el día hasta las 23:59:59.999... + }; + // El nombre del SP en el archivo es SP_DistCanillasNovedades + return await connection.QueryAsync( + "dbo.SP_DistCanillasNovedades", // Asegúrate que el nombre del SP sea exacto + parameters, + commandType: CommandType.StoredProcedure + ); + } + + public async Task> GetReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta) + { + using var connection = _connectionFactory.CreateConnection(); + var parameters = new + { + idEmpresa, + fechaDesde = fechaDesde.Date, + fechaHasta = fechaHasta.Date // El SP SP_DistCanillasGanancias maneja el rango inclusivo directamente + }; + return await connection.QueryAsync( + "dbo.SP_DistCanillasGanancias", // Nombre del SP + parameters, + commandType: CommandType.StoredProcedure + ); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs index 9ed14f5..c7c67bc 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/IReportesRepository.cs @@ -43,5 +43,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes Task<(IEnumerable Simple, IEnumerable Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta); Task> GetLiquidacionCanillaDetalleAsync(DateTime fecha, int idCanilla); Task> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla); + Task> GetReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); + Task> GetReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); } } \ 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 03b0504..8792ec9 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Reportes/ReportesRepository.cs @@ -481,39 +481,71 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes } public async Task> GetLiquidacionCanillaDetalleAsync(DateTime fecha, int idCanilla) - { - const string spName = "dbo.SP_DistCanillasLiquidacion"; - var parameters = new DynamicParameters(); - parameters.Add("@fecha", fecha, DbType.DateTime); - parameters.Add("@idCanilla", idCanilla, DbType.Int32); - try - { - using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + { + const string spName = "dbo.SP_DistCanillasLiquidacion"; + var parameters = new DynamicParameters(); + parameters.Add("@fecha", fecha, DbType.DateTime); + parameters.Add("@idCanilla", idCanilla, DbType.Int32); + try + { + using var connection = _dbConnectionFactory.CreateConnection(); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Detalle. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla); + return Enumerable.Empty(); + } } - catch (Exception ex) - { - _logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Detalle. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla); - return Enumerable.Empty(); - } - } - public async Task> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla) - { - const string spName = "dbo.SP_DistCanillasLiquidacionGanancias"; - var parameters = new DynamicParameters(); - parameters.Add("@fecha", fecha, DbType.DateTime); - parameters.Add("@idCanilla", idCanilla, DbType.Int32); - try - { - using var connection = _dbConnectionFactory.CreateConnection(); - return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + public async Task> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla) + { + const string spName = "dbo.SP_DistCanillasLiquidacionGanancias"; + var parameters = new DynamicParameters(); + parameters.Add("@fecha", fecha, DbType.DateTime); + parameters.Add("@idCanilla", idCanilla, DbType.Int32); + try + { + using var connection = _dbConnectionFactory.CreateConnection(); + return await connection.QueryAsync(spName, parameters, commandType: CommandType.StoredProcedure); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Ganancias. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla); + return Enumerable.Empty(); + } } - catch (Exception ex) - { - _logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Ganancias. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla); - return Enumerable.Empty(); + + public async Task> GetReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista) + { + using var connection = _dbConnectionFactory.CreateConnection(); + var parameters = new + { + fechaDesde = fechaDesde.Date, + fechaHasta = fechaHasta.Date, // El SP parece manejar el rango incluyendo el último día + accionista = esAccionista + }; + return await connection.QueryAsync( + "dbo.SP_DistCanillasAccConImporteEntreFechasDiarios", + parameters, + commandType: CommandType.StoredProcedure + ); + } + + public async Task> GetReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista) + { + using var connection = _dbConnectionFactory.CreateConnection(); + var parameters = new + { + fechaDesde = fechaDesde.Date, + fechaHasta = fechaHasta.Date, + accionista = esAccionista + }; + return await connection.QueryAsync( + "dbo.SP_DistCanillasAccConImporteEntreFechas", + parameters, + commandType: CommandType.StoredProcedure + ); } - } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/Saldo.cs b/Backend/GestionIntegral.Api/Models/Contables/Saldo.cs new file mode 100644 index 0000000..493821e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/Saldo.cs @@ -0,0 +1,9 @@ +public class Saldo +{ + public int IdSaldo { get; set; } + public string Destino { get; set; } = string.Empty; + public int IdDestino { get; set; } + public decimal Monto { get; set; } + public int IdEmpresa { get; set; } + public DateTime FechaUltimaModificacion { get; set; } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/SaldoAjusteHistorial.cs b/Backend/GestionIntegral.Api/Models/Contables/SaldoAjusteHistorial.cs new file mode 100644 index 0000000..a235205 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/SaldoAjusteHistorial.cs @@ -0,0 +1,19 @@ +using System; + +namespace GestionIntegral.Api.Models.Contables +{ + public class SaldoAjusteHistorial + { + public int IdSaldoAjusteHist { get; set; } // PK, Identity + public string Destino { get; set; } = string.Empty; + public int IdDestino { get; set; } + public int IdEmpresa { get; set; } + public decimal MontoAjuste { get; set; } // El monto que se sumó/restó + public decimal SaldoAnterior { get; set; } + public decimal SaldoNuevo { get; set; } + public string Justificacion { get; set; } = string.Empty; + public DateTime FechaAjuste { get; set; } + public int IdUsuarioAjuste { get; set; } + // Podrías añadir NombreUsuarioAjuste si quieres desnormalizar o hacer un JOIN al consultar + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/NovedadCanilla.cs b/Backend/GestionIntegral.Api/Models/Distribucion/NovedadCanilla.cs new file mode 100644 index 0000000..5c4b4b4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/NovedadCanilla.cs @@ -0,0 +1,10 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class NovedadCanilla + { + public int IdNovedad { get; set; } + public int IdCanilla { get; set; } + public DateTime Fecha { get; set; } + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/NovedadCanillaHistorial.cs b/Backend/GestionIntegral.Api/Models/Distribucion/NovedadCanillaHistorial.cs new file mode 100644 index 0000000..2e48a4d --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/NovedadCanillaHistorial.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class NovedadCanillaHistorial + { + // No tiene un ID propio, es una tabla de historial puro + public int IdNovedad { get; set; } // FK a la novedad original + public int IdCanilla { get; set; } + public DateTime Fecha { get; set; } + public string? Detalle { get; set; } + public int IdUsuario { get; set; } // Quién hizo el cambio + public DateTime FechaMod { get; set; } // Cuándo se hizo el cambio + public required string TipoMod { get; set; } // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/AjusteSaldoRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/AjusteSaldoRequestDto.cs new file mode 100644 index 0000000..13ed5b5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/AjusteSaldoRequestDto.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Contables +{ + public class AjusteSaldoRequestDto + { + [Required(ErrorMessage = "El tipo de destino es obligatorio ('Distribuidores' o 'Canillas').")] + [RegularExpression("^(Distribuidores|Canillas)$", ErrorMessage = "Destino debe ser 'Distribuidores' o 'Canillas'.")] + public string Destino { get; set; } = string.Empty; + + [Required(ErrorMessage = "El ID del destinatario es obligatorio.")] + [Range(1, int.MaxValue, ErrorMessage = "ID de Destinatario inválido.")] + public int IdDestino { get; set; } + + [Required(ErrorMessage = "El ID de la empresa es obligatorio.")] + [Range(1, int.MaxValue, ErrorMessage = "ID de Empresa inválido.")] + public int IdEmpresa { get; set; } + + [Required(ErrorMessage = "El monto del ajuste es obligatorio.")] + // Permitir montos negativos para disminuir deuda o positivos para aumentarla + // No se usa Range aquí para permitir ambos signos. La validación de que no sea cero se puede hacer en el servicio. + public decimal MontoAjuste { get; set; } + + [Required(ErrorMessage = "La justificación del ajuste es obligatoria.")] + [StringLength(250, MinimumLength = 5, ErrorMessage = "La justificación debe tener entre 5 y 250 caracteres.")] + public string Justificacion { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/SaldoGestionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/SaldoGestionDto.cs new file mode 100644 index 0000000..fa10da0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/SaldoGestionDto.cs @@ -0,0 +1,16 @@ +using System; + +namespace GestionIntegral.Api.Dtos.Contables +{ + public class SaldoGestionDto + { + public int IdSaldo { get; set; } + public string Destino { get; set; } = string.Empty; + public int IdDestino { get; set; } + public string NombreDestinatario { get; set; } = string.Empty; + public int IdEmpresa { get; set; } + public string NombreEmpresa { get; set; } = string.Empty; + public decimal Monto { get; set; } + public DateTime FechaUltimaModificacion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateNovedadCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateNovedadCanillaDto.cs new file mode 100644 index 0000000..1964d3b --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateNovedadCanillaDto.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateNovedadCanillaDto + { + // IdCanilla se tomará de la ruta o de un campo oculto si el POST es a un endpoint genérico. + // Por ahora, lo incluimos si el endpoint es /api/novedadescanilla + [Required] + public int IdCanilla { get; set; } + + [Required] + public DateTime Fecha { get; set; } + + [MaxLength(250)] + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDropdownDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDropdownDto.cs new file mode 100644 index 0000000..0d7d792 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDropdownDto.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class DistribuidorDropdownDto + { + public int IdDistribuidor { get; set; } + public string Nombre { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorLookupDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorLookupDto.cs new file mode 100644 index 0000000..1901643 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorLookupDto.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class DistribuidorLookupDto + { + public int IdDistribuidor { get; set; } + public string Nombre { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaDropdownDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaDropdownDto.cs new file mode 100644 index 0000000..907bdd8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaDropdownDto.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Dtos.Empresas +{ + public class EmpresaDropdownDto + { + public int IdEmpresa { get; set; } + public string Nombre { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaLookupDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaLookupDto.cs new file mode 100644 index 0000000..386e377 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaLookupDto.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Dtos.Empresas +{ + public class EmpresaLookupDto + { + public int IdEmpresa { get; set; } + public string Nombre { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/NovedadCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/NovedadCanillaDto.cs new file mode 100644 index 0000000..a40ab64 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/NovedadCanillaDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class NovedadCanillaDto + { + public int IdNovedad { get; set; } + public int IdCanilla { get; set; } + public string NombreCanilla { get; set; } = string.Empty; // Para mostrar en UI + public DateTime Fecha { get; set; } + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateNovedadCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateNovedadCanillaDto.cs new file mode 100644 index 0000000..8526dcc --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateNovedadCanillaDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateNovedadCanillaDto + { + // No se permite cambiar IdCanilla ni Fecha + [MaxLength(250)] + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/CanillaGananciaReporteDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/CanillaGananciaReporteDto.cs new file mode 100644 index 0000000..f72dc84 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/CanillaGananciaReporteDto.cs @@ -0,0 +1,11 @@ +namespace GestionIntegral.Api.Dtos.Reportes +{ + public class CanillaGananciaReporteDto // Nuevo nombre para el DTO + { + public string Canilla { get; set; } = string.Empty; // NomApe del canillita + public int? Legajo { get; set; } + public int? Francos { get; set; } + public int? Faltas { get; set; } + public decimal? TotalRendir { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ListadoDistCanMensualDiariosDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ListadoDistCanMensualDiariosDto.cs new file mode 100644 index 0000000..eb5b619 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ListadoDistCanMensualDiariosDto.cs @@ -0,0 +1,13 @@ +namespace GestionIntegral.Api.Dtos.Reportes +{ + public class ListadoDistCanMensualDiariosDto + { + public string Canilla { get; set; } = string.Empty; + public int? ElDia { get; set; } // Cantidad + public int? ElPlata { get; set; } // Cantidad + public int? Vendidos { get; set; } // Suma de ElDia y ElPlata (cantidades) + public decimal? ImporteElDia { get; set; } + public decimal? ImporteElPlata { get; set; } + public decimal? ImporteTotal { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ListadoDistCanMensualPubDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ListadoDistCanMensualPubDto.cs new file mode 100644 index 0000000..6bc9fdb --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ListadoDistCanMensualPubDto.cs @@ -0,0 +1,13 @@ +namespace GestionIntegral.Api.Dtos.Reportes +{ + public class ListadoDistCanMensualPubDto + { + public string Publicacion { get; set; } = string.Empty; + public string Canilla { get; set; } = string.Empty; // NomApe + public int? TotalCantSalida { get; set; } + public int? TotalCantEntrada { get; set; } + public decimal? TotalRendir { get; set; } + // No es necesario 'Vendidos' ya que el SP no lo devuelve directamente para esta variante, + // pero se puede calcular en el frontend si es necesario (Llevados - Devueltos). + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ReporteNovedadCanillaItemDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ReporteNovedadCanillaItemDto.cs new file mode 100644 index 0000000..f555768 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Reportes/ReporteNovedadCanillaItemDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace GestionIntegral.Api.Dtos.Reportes +{ + public class NovedadesCanillasReporteDto + { + public string NomApe { get; set; } = string.Empty; // Nombre del Canillita + public DateTime Fecha { get; set; } + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 206c5dd..c98bb7d 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -82,6 +82,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +// Servicio de Saldos +builder.Services.AddScoped(); // Repositorios de Reportes builder.Services.AddScoped(); // Servicios de Reportes @@ -199,7 +203,7 @@ if (app.Environment.IsDevelopment()) }); } -// ¡¡¡NO USAR UseHttpsRedirection si tu API corre en HTTP!!! +// ¡¡¡NO USAR UseHttpsRedirection si la API corre en HTTP!!! // Comenta o elimina la siguiente línea si SÓLO usas http://localhost:5183 // app.UseHttpsRedirection(); diff --git a/Backend/GestionIntegral.Api/Services/Contables/ISaldoService.cs b/Backend/GestionIntegral.Api/Services/Contables/ISaldoService.cs new file mode 100644 index 0000000..d16c618 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/ISaldoService.cs @@ -0,0 +1,12 @@ +using GestionIntegral.Api.Dtos.Contables; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Contables +{ + public interface ISaldoService + { + Task> ObtenerSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter); + Task<(bool Exito, string? Error, SaldoGestionDto? SaldoActualizado)> RealizarAjusteManualSaldoAsync(AjusteSaldoRequestDto ajusteDto, int idUsuarioAjuste); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs b/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs index 8b27f5d..da162cd 100644 --- a/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs +++ b/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs @@ -52,7 +52,7 @@ namespace GestionIntegral.Api.Services.Contables } else if (nota.Destino == "Canillas") { - var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino); // Asumiendo que GetByIdAsync devuelve una tupla + var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino); nombreDestinatario = canData.Canilla?.NomApe ?? "Canillita Desconocido"; } @@ -95,7 +95,6 @@ namespace GestionIntegral.Api.Services.Contables public async Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario) { - // Validar Destinatario if (createDto.Destino == "Distribuidores") { if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) @@ -103,7 +102,7 @@ namespace GestionIntegral.Api.Services.Contables } else if (createDto.Destino == "Canillas") { - if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) // Asumiendo GetByIdSimpleAsync en ICanillaRepository + if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) return (null, "El canillita especificado no existe."); } else { return (null, "Tipo de destino inválido."); } @@ -124,19 +123,29 @@ namespace GestionIntegral.Api.Services.Contables }; using var connection = _connectionFactory.CreateConnection(); - if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); - using var transaction = connection.BeginTransaction(); + IDbTransaction? transaction = null; try { + if (connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } + transaction = connection.BeginTransaction(); + var notaCreada = await _notaRepo.CreateAsync(nuevaNota, idUsuario, transaction); if (notaCreada == null) throw new DataException("Error al registrar la nota."); - // Afectar Saldo - // Nota de Crédito: Disminuye la deuda del destinatario (monto positivo para el servicio de saldo) - // Nota de Débito: Aumenta la deuda del destinatario (monto negativo para el servicio de saldo) - decimal montoAjusteSaldo = createDto.Tipo == "Credito" ? createDto.Monto : -createDto.Monto; + decimal montoParaSaldo; + if (createDto.Tipo == "Credito") + { + montoParaSaldo = -createDto.Monto; + } + else + { + montoParaSaldo = createDto.Monto; + } - bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoAjusteSaldo, transaction); + bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoParaSaldo, transaction); if (!saldoActualizado) throw new DataException($"Error al actualizar el saldo para {notaCreada.Destino} ID {notaCreada.IdDestino}."); transaction.Commit(); @@ -145,32 +154,57 @@ namespace GestionIntegral.Api.Services.Contables } catch (Exception ex) { - try { transaction.Rollback(); } catch { } + try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearAsync NotaCreditoDebito."); } _logger.LogError(ex, "Error CrearAsync NotaCreditoDebito."); return (null, $"Error interno: {ex.Message}"); } + finally + { + if (connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + } } public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto updateDto, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); - if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); - using var transaction = connection.BeginTransaction(); + IDbTransaction? transaction = null; try { - var notaExistente = await _notaRepo.GetByIdAsync(idNota); - if (notaExistente == null) return (false, "Nota no encontrada."); + if (connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } + transaction = connection.BeginTransaction(); + + var notaExistente = await _notaRepo.GetByIdAsync(idNota); + if (notaExistente == null) + { + transaction.Rollback(); + return (false, "Nota no encontrada."); + } + + decimal impactoOriginalSaldo = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto; + decimal impactoNuevoSaldo = notaExistente.Tipo == "Credito" ? -updateDto.Monto : updateDto.Monto; + decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo; - // Calcular diferencia de monto para ajustar saldo - decimal montoOriginal = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto; - decimal montoNuevo = notaExistente.Tipo == "Credito" ? updateDto.Monto : -updateDto.Monto; // Tipo no cambia - decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal; + var notaParaActualizarEnRepo = new NotaCreditoDebito + { + IdNota = notaExistente.IdNota, + Destino = notaExistente.Destino, + IdDestino = notaExistente.IdDestino, + Referencia = notaExistente.Referencia, + Tipo = notaExistente.Tipo, + Fecha = notaExistente.Fecha, + Monto = updateDto.Monto, + Observaciones = updateDto.Observaciones, + IdEmpresa = notaExistente.IdEmpresa + }; - notaExistente.Monto = updateDto.Monto; - notaExistente.Observaciones = updateDto.Observaciones; - - var actualizado = await _notaRepo.UpdateAsync(notaExistente, idUsuario, transaction); - if (!actualizado) throw new DataException("Error al actualizar la nota."); + var actualizado = await _notaRepo.UpdateAsync(notaParaActualizarEnRepo, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar la nota en la base de datos."); if (diferenciaAjusteSaldo != 0) { @@ -182,30 +216,45 @@ namespace GestionIntegral.Api.Services.Contables _logger.LogInformation("NotaC/D ID {Id} actualizada por Usuario ID {UserId}.", idNota, idUsuario); return (true, null); } - catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); } + catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync NotaCreditoDebito (KeyNotFound)."); } return (false, "Nota no encontrada."); } catch (Exception ex) { - try { transaction.Rollback(); } catch { } - _logger.LogError(ex, "Error ActualizarAsync NotaC/D ID: {Id}", idNota); + try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync NotaCreditoDebito."); } + _logger.LogError(ex, "Error ActualizarAsync Nota C/D ID: {Id}", idNota); return (false, $"Error interno: {ex.Message}"); } + finally + { + if (connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + } } public async Task<(bool Exito, string? Error)> EliminarAsync(int idNota, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); - if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); - using var transaction = connection.BeginTransaction(); + IDbTransaction? transaction = null; try { - var notaExistente = await _notaRepo.GetByIdAsync(idNota); - if (notaExistente == null) return (false, "Nota no encontrada."); + if (connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } + transaction = connection.BeginTransaction(); - // Revertir el efecto en el saldo - decimal montoReversion = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto; + var notaExistente = await _notaRepo.GetByIdAsync(idNota); + if (notaExistente == null) + { + transaction.Rollback(); + return (false, "Nota no encontrada."); + } + + decimal montoReversion = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto; var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction); - if (!eliminado) throw new DataException("Error al eliminar la nota."); + if (!eliminado) throw new DataException("Error al eliminar la nota de la base de datos."); bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, montoReversion, transaction); if (!saldoActualizado) throw new DataException("Error al revertir el saldo tras la eliminación de la nota."); @@ -214,13 +263,20 @@ namespace GestionIntegral.Api.Services.Contables _logger.LogInformation("NotaC/D ID {Id} eliminada y saldo revertido por Usuario ID {UserId}.", idNota, idUsuario); return (true, null); } - catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); } + catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync NotaCreditoDebito (KeyNotFound)."); } return (false, "Nota no encontrada."); } catch (Exception ex) { - try { transaction.Rollback(); } catch { } + try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync NotaCreditoDebito."); } _logger.LogError(ex, "Error EliminarAsync NotaC/D ID: {Id}", idNota); return (false, $"Error interno: {ex.Message}"); } + finally + { + if (connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + } } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs index 6552435..2f36916 100644 --- a/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs +++ b/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs @@ -95,7 +95,6 @@ namespace GestionIntegral.Api.Services.Contables if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento)) return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'."); - var nuevoPago = new PagoDistribuidor { IdDistribuidor = createDto.IdDistribuidor, @@ -109,19 +108,29 @@ namespace GestionIntegral.Api.Services.Contables }; using var connection = _connectionFactory.CreateConnection(); - if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); - using var transaction = connection.BeginTransaction(); + IDbTransaction? transaction = null; // Declarar fuera para el finally try { + if (connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } + transaction = connection.BeginTransaction(); + var pagoCreado = await _pagoRepo.CreateAsync(nuevoPago, idUsuario, transaction); if (pagoCreado == null) throw new DataException("Error al registrar el pago."); - // Afectar Saldo - // Si TipoMovimiento es "Recibido", el monto DISMINUYE la deuda del distribuidor (monto positivo para el servicio de saldo). - // Si TipoMovimiento es "Realizado" (empresa paga a distribuidor), el monto AUMENTA la deuda (monto negativo para el servicio de saldo). - decimal montoAjusteSaldo = createDto.TipoMovimiento == "Recibido" ? createDto.Monto : -createDto.Monto; + decimal montoParaSaldo; + if (createDto.TipoMovimiento == "Recibido") + { + montoParaSaldo = -createDto.Monto; + } + else + { + montoParaSaldo = createDto.Monto; + } - bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoCreado.IdDistribuidor, pagoCreado.IdEmpresa, montoAjusteSaldo, transaction); + bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoCreado.IdDistribuidor, pagoCreado.IdEmpresa, montoParaSaldo, transaction); if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor."); transaction.Commit(); @@ -130,37 +139,63 @@ namespace GestionIntegral.Api.Services.Contables } catch (Exception ex) { - try { transaction.Rollback(); } catch { } + try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearAsync PagoDistribuidor."); } _logger.LogError(ex, "Error CrearAsync PagoDistribuidor."); return (null, $"Error interno: {ex.Message}"); } + finally + { + if (connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + } } public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPago, UpdatePagoDistribuidorDto updateDto, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); - if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); - using var transaction = connection.BeginTransaction(); + IDbTransaction? transaction = null; try { - var pagoExistente = await _pagoRepo.GetByIdAsync(idPago); - if (pagoExistente == null) return (false, "Pago no encontrado."); + if (connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } + transaction = connection.BeginTransaction(); + + var pagoExistente = await _pagoRepo.GetByIdAsync(idPago); + if (pagoExistente == null) + { + transaction.Rollback(); // Rollback si no se encuentra + return (false, "Pago no encontrado."); + } if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null) + { + transaction.Rollback(); return (false, "Tipo de pago no válido."); + } + + decimal impactoOriginalSaldo = pagoExistente.TipoMovimiento == "Recibido" ? -pagoExistente.Monto : pagoExistente.Monto; + decimal impactoNuevoSaldo = pagoExistente.TipoMovimiento == "Recibido" ? -updateDto.Monto : updateDto.Monto; + decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo; - // Calcular la diferencia de monto para ajustar el saldo - decimal montoOriginal = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto; - decimal montoNuevo = pagoExistente.TipoMovimiento == "Recibido" ? updateDto.Monto : -updateDto.Monto; - decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal; + var pagoParaActualizarEnRepo = new PagoDistribuidor + { + IdPago = pagoExistente.IdPago, + IdDistribuidor = pagoExistente.IdDistribuidor, + Fecha = pagoExistente.Fecha, + TipoMovimiento = pagoExistente.TipoMovimiento, + Recibo = pagoExistente.Recibo, + Monto = updateDto.Monto, + IdTipoPago = updateDto.IdTipoPago, + Detalle = updateDto.Detalle, + IdEmpresa = pagoExistente.IdEmpresa + }; - // Actualizar campos permitidos - pagoExistente.Monto = updateDto.Monto; - pagoExistente.IdTipoPago = updateDto.IdTipoPago; - pagoExistente.Detalle = updateDto.Detalle; - - var actualizado = await _pagoRepo.UpdateAsync(pagoExistente, idUsuario, transaction); - if (!actualizado) throw new DataException("Error al actualizar el pago."); + var actualizado = await _pagoRepo.UpdateAsync(pagoParaActualizarEnRepo, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar el pago en la base de datos."); if (diferenciaAjusteSaldo != 0) { @@ -172,32 +207,45 @@ namespace GestionIntegral.Api.Services.Contables _logger.LogInformation("PagoDistribuidor ID {Id} actualizado por Usuario ID {UserId}.", idPago, idUsuario); return (true, null); } - catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); } + catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); } catch (Exception ex) { - try { transaction.Rollback(); } catch { } + try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor."); } _logger.LogError(ex, "Error ActualizarAsync PagoDistribuidor ID: {Id}", idPago); return (false, $"Error interno: {ex.Message}"); } + finally + { + if (connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + } } public async Task<(bool Exito, string? Error)> EliminarAsync(int idPago, int idUsuario) { using var connection = _connectionFactory.CreateConnection(); - if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); - using var transaction = connection.BeginTransaction(); + IDbTransaction? transaction = null; try { + if (connection.State != ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + } + transaction = connection.BeginTransaction(); + var pagoExistente = await _pagoRepo.GetByIdAsync(idPago); - if (pagoExistente == null) return (false, "Pago no encontrado."); - - // Revertir el efecto en el saldo - // Si fue "Recibido", el saldo disminuyó (montoAjusteSaldo fue +Monto). Al eliminar, revertimos sumando -Monto (o restando +Monto). - // Si fue "Realizado", el saldo aumentó (montoAjusteSaldo fue -Monto). Al eliminar, revertimos sumando +Monto (o restando -Monto). - decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? -pagoExistente.Monto : pagoExistente.Monto; + if (pagoExistente == null) + { + transaction.Rollback(); + return (false, "Pago no encontrado."); + } + + decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto; var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction); - if (!eliminado) throw new DataException("Error al eliminar el pago."); + if (!eliminado) throw new DataException("Error al eliminar el pago de la base de datos."); bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, montoReversion, transaction); if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación del pago."); @@ -206,13 +254,20 @@ namespace GestionIntegral.Api.Services.Contables _logger.LogInformation("PagoDistribuidor ID {Id} eliminado por Usuario ID {UserId}.", idPago, idUsuario); return (true, null); } - catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); } + catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); } catch (Exception ex) { - try { transaction.Rollback(); } catch { } + try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor."); } _logger.LogError(ex, "Error EliminarAsync PagoDistribuidor ID: {Id}", idPago); return (false, $"Error interno: {ex.Message}"); } + finally + { + if (connection.State == ConnectionState.Open) + { + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); + } + } } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/SaldoService.cs b/Backend/GestionIntegral.Api/Services/Contables/SaldoService.cs new file mode 100644 index 0000000..5d63568 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/SaldoService.cs @@ -0,0 +1,164 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Contables; +using GestionIntegral.Api.Data.Repositories.Distribucion; // Para IDistribuidorRepository, ICanillaRepository +using GestionIntegral.Api.Dtos.Contables; +using GestionIntegral.Api.Models.Contables; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Contables +{ + public class SaldoService : ISaldoService + { + private readonly ISaldoRepository _saldoRepo; + private readonly IDistribuidorRepository _distribuidorRepo; // Para nombres + private readonly ICanillaRepository _canillaRepo; // Para nombres + private readonly IEmpresaRepository _empresaRepo; // Para nombres + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public SaldoService( + ISaldoRepository saldoRepo, + IDistribuidorRepository distribuidorRepo, + ICanillaRepository canillaRepo, + IEmpresaRepository empresaRepo, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _saldoRepo = saldoRepo; + _distribuidorRepo = distribuidorRepo; + _canillaRepo = canillaRepo; + _empresaRepo = empresaRepo; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task MapToGestionDto(Saldo saldo) + { + if (saldo == null) return null!; + + string nombreDestinatario = "N/A"; + if (saldo.Destino == "Distribuidores") + { + var distData = await _distribuidorRepo.GetByIdAsync(saldo.IdDestino); + nombreDestinatario = distData.Distribuidor?.Nombre ?? $"Dist. ID {saldo.IdDestino}"; + } + else if (saldo.Destino == "Canillas") + { + var canData = await _canillaRepo.GetByIdAsync(saldo.IdDestino); + nombreDestinatario = canData.Canilla?.NomApe ?? $"Can. ID {saldo.IdDestino}"; + } + + var empresa = await _empresaRepo.GetByIdAsync(saldo.IdEmpresa); + + return new SaldoGestionDto + { + IdSaldo = saldo.IdSaldo, + Destino = saldo.Destino, + IdDestino = saldo.IdDestino, + NombreDestinatario = nombreDestinatario, + IdEmpresa = saldo.IdEmpresa, + NombreEmpresa = empresa?.Nombre ?? $"Emp. ID {saldo.IdEmpresa}", + Monto = saldo.Monto, + FechaUltimaModificacion = saldo.FechaUltimaModificacion + }; + } + + public async Task> ObtenerSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter) + { + var saldos = await _saldoRepo.GetSaldosParaGestionAsync(destinoFilter, idDestinoFilter, idEmpresaFilter); + var dtos = new List(); + foreach (var saldo in saldos) + { + dtos.Add(await MapToGestionDto(saldo)); + } + return dtos; + } + + public async Task<(bool Exito, string? Error, SaldoGestionDto? SaldoActualizado)> RealizarAjusteManualSaldoAsync(AjusteSaldoRequestDto ajusteDto, int idUsuarioAjuste) + { + if (ajusteDto.MontoAjuste == 0) + return (false, "El monto de ajuste no puede ser cero.", null); + + // Validar existencia de Destino y Empresa + if (ajusteDto.Destino == "Distribuidores") + { + if (await _distribuidorRepo.GetByIdSimpleAsync(ajusteDto.IdDestino) == null) + return (false, "El distribuidor especificado no existe.", null); + } + else if (ajusteDto.Destino == "Canillas") + { + if (await _canillaRepo.GetByIdSimpleAsync(ajusteDto.IdDestino) == null) + return (false, "El canillita especificado no existe.", null); + } else { + return (false, "Tipo de destino inválido.", null); + } + if (await _empresaRepo.GetByIdAsync(ajusteDto.IdEmpresa) == null) + return (false, "La empresa especificada no existe.", null); + + + using var connection = _connectionFactory.CreateConnection(); + if (connection.State != ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); } + using var transaction = connection.BeginTransaction(); + try + { + var saldoActual = await _saldoRepo.GetSaldoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, transaction); + if (saldoActual == null) + { + // Podríamos crear el saldo aquí si no existe y se quiere permitir un ajuste sobre un saldo nuevo. + // O devolver error. Por ahora, error. + transaction.Rollback(); + return (false, "No se encontró un saldo existente para el destinatario y empresa especificados.", null); + } + + decimal saldoAnterior = saldoActual.Monto; + + bool modificado = await _saldoRepo.ModificarSaldoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, ajusteDto.MontoAjuste, transaction); + if (!modificado) + { + throw new DataException("No se pudo modificar el saldo principal."); + } + + // Obtener el saldo después de la modificación para el historial + var saldoDespuesDeModificacion = await _saldoRepo.GetSaldoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, transaction); + if(saldoDespuesDeModificacion == null) throw new DataException("No se pudo obtener el saldo después de la modificación."); + + + var historial = new SaldoAjusteHistorial + { + Destino = ajusteDto.Destino, + IdDestino = ajusteDto.IdDestino, + IdEmpresa = ajusteDto.IdEmpresa, + MontoAjuste = ajusteDto.MontoAjuste, + SaldoAnterior = saldoAnterior, + SaldoNuevo = saldoDespuesDeModificacion.Monto, // saldoActual.Monto + ajusteDto.MontoAjuste, + Justificacion = ajusteDto.Justificacion, + FechaAjuste = DateTime.Now, // O UtcNow + IdUsuarioAjuste = idUsuarioAjuste + }; + await _saldoRepo.CreateSaldoAjusteHistorialAsync(historial, transaction); + + transaction.Commit(); + _logger.LogInformation("Ajuste manual de saldo realizado para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa} por Usuario ID {IdUsuarioAjuste}. Monto: {MontoAjuste}", + ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, idUsuarioAjuste, ajusteDto.MontoAjuste); + + var saldoDtoActualizado = await MapToGestionDto(saldoDespuesDeModificacion); + return (true, null, saldoDtoActualizado); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx){ _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync."); } + _logger.LogError(ex, "Error en RealizarAjusteManualSaldoAsync."); + return (false, $"Error interno al realizar el ajuste: {ex.Message}", null); + } + finally + { + if (connection.State == ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); } + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs index 9ef99ce..a9a4456 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs @@ -54,11 +54,11 @@ namespace GestionIntegral.Api.Services.Distribucion }; } - public async Task> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos) + public async Task> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos) { - var canillasData = await _canillaRepository.GetAllAsync(nomApeFilter, legajoFilter, soloActivos); + var data = await _canillaRepository.GetAllAsync(nomApeFilter, legajoFilter, soloActivos, esAccionista); // Filtrar nulos y asegurar al compilador que no hay nulos en la lista final - return canillasData.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); + return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); } public async Task ObtenerPorIdAsync(int id) @@ -81,11 +81,11 @@ namespace GestionIntegral.Api.Services.Distribucion } if (createDto.Empresa != 0) // Solo validar empresa si no es 0 { - var empresa = await _empresaRepository.GetByIdAsync(createDto.Empresa); - if(empresa == null) - { + var empresa = await _empresaRepository.GetByIdAsync(createDto.Empresa); + if (empresa == null) + { return (null, "La empresa seleccionada no es válida."); - } + } } // CORREGIDO: Usar directamente el valor booleano @@ -122,7 +122,7 @@ namespace GestionIntegral.Api.Services.Distribucion if (canillaCreado == null) throw new DataException("Error al crear el canillita."); transaction.Commit(); - + // Para el DTO de respuesta, necesitamos NombreZona y NombreEmpresa string nombreEmpresaParaDto = "N/A (Accionista)"; if (canillaCreado.Empresa != 0) @@ -131,12 +131,20 @@ namespace GestionIntegral.Api.Services.Distribucion nombreEmpresaParaDto = empresaData?.Nombre ?? "Empresa Desconocida"; } - var dtoCreado = new CanillaDto { - IdCanilla = canillaCreado.IdCanilla, Legajo = canillaCreado.Legajo, NomApe = canillaCreado.NomApe, - Parada = canillaCreado.Parada, IdZona = canillaCreado.IdZona, NombreZona = zona.Nombre, // Usar nombre de zona ya obtenido - Accionista = canillaCreado.Accionista, Obs = canillaCreado.Obs, Empresa = canillaCreado.Empresa, - NombreEmpresa = nombreEmpresaParaDto, - Baja = canillaCreado.Baja, FechaBaja = null + var dtoCreado = new CanillaDto + { + IdCanilla = canillaCreado.IdCanilla, + Legajo = canillaCreado.Legajo, + NomApe = canillaCreado.NomApe, + Parada = canillaCreado.Parada, + IdZona = canillaCreado.IdZona, + NombreZona = zona.Nombre, // Usar nombre de zona ya obtenido + Accionista = canillaCreado.Accionista, + Obs = canillaCreado.Obs, + Empresa = canillaCreado.Empresa, + NombreEmpresa = nombreEmpresaParaDto, + Baja = canillaCreado.Baja, + FechaBaja = null }; _logger.LogInformation("Canilla ID {IdCanilla} creado por Usuario ID {IdUsuario}.", canillaCreado.IdCanilla, idUsuario); @@ -144,7 +152,7 @@ namespace GestionIntegral.Api.Services.Distribucion } catch (Exception ex) { - try { transaction.Rollback(); } catch {} + try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error CrearAsync Canilla: {NomApe}", createDto.NomApe); return (null, $"Error interno al crear el canillita: {ex.Message}"); } @@ -165,11 +173,11 @@ namespace GestionIntegral.Api.Services.Distribucion } if (updateDto.Empresa != 0) // Solo validar empresa si no es 0 { - var empresa = await _empresaRepository.GetByIdAsync(updateDto.Empresa); - if(empresa == null) - { + var empresa = await _empresaRepository.GetByIdAsync(updateDto.Empresa); + if (empresa == null) + { return (false, "La empresa seleccionada no es válida."); - } + } } // Usar directamente el valor booleano para Accionista @@ -200,18 +208,19 @@ namespace GestionIntegral.Api.Services.Distribucion try { var actualizado = await _canillaRepository.UpdateAsync(canillaExistente, idUsuario, transaction); - if (!actualizado) throw new DataException("Error al actualizar el canillita."); + if (!actualizado) throw new DataException("Error al actualizar el canillita."); transaction.Commit(); _logger.LogInformation("Canilla ID {IdCanilla} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); return (true, null); } - catch (KeyNotFoundException) { - try { transaction.Rollback(); } catch {} + catch (KeyNotFoundException) + { + try { transaction.Rollback(); } catch { } return (false, "Canillita no encontrado durante la actualización."); } catch (Exception ex) { - try { transaction.Rollback(); } catch {} + try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error ActualizarAsync Canilla ID: {IdCanilla}", id); return (false, $"Error interno al actualizar el canillita: {ex.Message}"); } @@ -240,13 +249,14 @@ namespace GestionIntegral.Api.Services.Distribucion _logger.LogInformation("Estado de baja cambiado a {EstadoBaja} para Canilla ID {IdCanilla} por Usuario ID {IdUsuario}.", darDeBaja, id, idUsuario); return (true, null); } - catch (KeyNotFoundException) { - try { transaction.Rollback(); } catch {} + catch (KeyNotFoundException) + { + try { transaction.Rollback(); } catch { } return (false, "Canillita no encontrado durante el cambio de estado de baja."); } catch (Exception ex) { - try { transaction.Rollback(); } catch {} + try { transaction.Rollback(); } catch { } _logger.LogError(ex, "Error ToggleBajaAsync Canilla ID: {IdCanilla}", id); return (false, $"Error interno al cambiar estado de baja: {ex.Message}"); } diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs index a1f1ca6..1a897c7 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs @@ -66,11 +66,31 @@ namespace GestionIntegral.Api.Services.Distribucion return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); } + public async Task> GetAllDropdownAsync() + { + var data = await _distribuidorRepository.GetAllDropdownAsync(); + // Asegurar que el resultado no sea nulo y no contiene elementos nulos + if (data == null) + { + return new List + { + new DistribuidorDropdownDto { IdDistribuidor = 0, Nombre = "No hay distribuidores disponibles" } + }; + } + return data.Where(x => x != null)!; + } + public async Task ObtenerPorIdAsync(int id) { var data = await _distribuidorRepository.GetByIdAsync(id); // MapToDto ahora devuelve DistribuidorDto? return MapToDto(data); + } + + public async Task ObtenerLookupPorIdAsync(int id) + { + var data = await _distribuidorRepository.ObtenerLookupPorIdAsync(id); + return data; } public async Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario) diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs index fda32f3..96a5abb 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs @@ -42,10 +42,22 @@ namespace GestionIntegral.Api.Services.Distribucion Detalle = e.Detalle }); } + + public async Task> ObtenerParaDropdown() + { + // El repositorio ya devuelve solo las activas si es necesario + var empresas = await _empresaRepository.GetAllDropdownAsync(); + // Mapeo Entidad -> DTO + return empresas.Select(e => new EmpresaDropdownDto + { + IdEmpresa = e.IdEmpresa, + Nombre = e.Nombre + }); + } public async Task ObtenerPorIdAsync(int id) { - // El repositorio ya devuelve solo las activas si es necesario + // El repositorio ya devuelve solo las activas si es necesario var empresa = await _empresaRepository.GetByIdAsync(id); if (empresa == null) return null; // Mapeo Entidad -> DTO @@ -57,6 +69,19 @@ namespace GestionIntegral.Api.Services.Distribucion }; } + public async Task ObtenerLookupPorIdAsync(int id) + { + // El repositorio ya devuelve solo las activas si es necesario + var empresa = await _empresaRepository.ObtenerLookupPorIdAsync(id); + if (empresa == null) return null; + // Mapeo Entidad -> DTO + return new EmpresaLookupDto + { + IdEmpresa = empresa.IdEmpresa, + Nombre = empresa.Nombre + }; + } + public async Task<(EmpresaDto? Empresa, string? Error)> CrearAsync(CreateEmpresaDto createDto, int idUsuario) { // Validación de negocio: Nombre duplicado @@ -234,5 +259,5 @@ namespace GestionIntegral.Api.Services.Distribucion } // --- Fin Transacción --- } - } + } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs index 9d69df5..89307ae 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs @@ -6,7 +6,7 @@ namespace GestionIntegral.Api.Services.Distribucion { public interface ICanillaService { - Task> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos); + Task> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos); Task ObtenerPorIdAsync(int id); Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario); diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs index 0512897..40a55c7 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs @@ -11,5 +11,7 @@ namespace GestionIntegral.Api.Services.Distribucion Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario); Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + Task> GetAllDropdownAsync(); + Task ObtenerLookupPorIdAsync(int id); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs index 328e235..9974495 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs @@ -11,5 +11,7 @@ namespace GestionIntegral.Api.Services.Distribucion Task<(EmpresaDto? Empresa, string? Error)> CrearAsync(CreateEmpresaDto createDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEmpresaDto updateDto, int idUsuario); Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + Task> ObtenerParaDropdown(); + Task ObtenerLookupPorIdAsync(int id); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/INovedadCanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/INovedadCanillaService.cs new file mode 100644 index 0000000..83d8ead --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/INovedadCanillaService.cs @@ -0,0 +1,19 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Dtos.Reportes; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface INovedadCanillaService + { + Task> ObtenerPorCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta); + Task ObtenerPorIdAsync(int idNovedad); + Task<(NovedadCanillaDto? Novedad, string? Error)> CrearAsync(CreateNovedadCanillaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idNovedad, UpdateNovedadCanillaDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idNovedad, int idUsuario); + Task> ObtenerReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); + Task> ObtenerReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/NovedadCanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/NovedadCanillaService.cs new file mode 100644 index 0000000..6089dd7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/NovedadCanillaService.cs @@ -0,0 +1,254 @@ +// En Services/Distribucion (o donde corresponda) +using GestionIntegral.Api.Data; // Para DbConnectionFactory +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Dtos.Reportes; +using GestionIntegral.Api.Models.Distribucion; // Asegúrate que el modelo Canilla tenga NomApe +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; // Para IDbTransaction +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class NovedadCanillaService : INovedadCanillaService + { + private readonly INovedadCanillaRepository _novedadRepository; + private readonly ICanillaRepository _canillaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public NovedadCanillaService( + INovedadCanillaRepository novedadRepository, + ICanillaRepository canillaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _novedadRepository = novedadRepository; + _canillaRepository = canillaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private NovedadCanillaDto MapToDto((NovedadCanilla Novedad, string NombreCanilla) data) + { + return new NovedadCanillaDto + { + IdNovedad = data.Novedad.IdNovedad, + IdCanilla = data.Novedad.IdCanilla, + NombreCanilla = data.NombreCanilla, // Viene de la tupla en GetByCanillaAsync + Fecha = data.Novedad.Fecha, + Detalle = data.Novedad.Detalle + }; + } + private NovedadCanillaDto MapToDto(NovedadCanilla data, string nombreCanilla) + { + return new NovedadCanillaDto + { + IdNovedad = data.IdNovedad, + IdCanilla = data.IdCanilla, + NombreCanilla = nombreCanilla, + Fecha = data.Fecha, + Detalle = data.Detalle + }; + } + + public async Task> ObtenerPorCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta) + { + var data = await _novedadRepository.GetByCanillaAsync(idCanilla, fechaDesde, fechaHasta); + return data.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int idNovedad) + { + var novedad = await _novedadRepository.GetByIdAsync(idNovedad); + if (novedad == null) return null; + + // Asumiendo que _canillaRepository.GetByIdAsync devuelve una tupla (Canilla? Canilla, ...) + // O un DTO CanillaDto que tiene NomApe + var canillaDataResult = await _canillaRepository.GetByIdAsync(novedad.IdCanilla); + + // Ajusta esto según lo que realmente devuelva GetByIdAsync + // Si devuelve CanillaDto: + // string nombreCanilla = canillaDataResult?.NomApe ?? "Desconocido"; + // Si devuelve la tupla (Canilla? Canilla, string? NombreZona, string? NombreEmpresa): + string nombreCanilla = canillaDataResult.Canilla?.NomApe ?? "Desconocido"; + + return MapToDto(novedad, nombreCanilla); + } + + public async Task<(NovedadCanillaDto? Novedad, string? Error)> CrearAsync(CreateNovedadCanillaDto createDto, int idUsuario) + { + // Asegúrate que GetByIdSimpleAsync devuelva un objeto Canilla o algo con NomApe + var canilla = await _canillaRepository.GetByIdSimpleAsync(createDto.IdCanilla); + if (canilla == null) + { + return (null, "El canillita especificado no existe."); + } + + var nuevaNovedad = new NovedadCanilla + { + IdCanilla = createDto.IdCanilla, + Fecha = createDto.Fecha.Date, + Detalle = createDto.Detalle + }; + + using var connection = _connectionFactory.CreateConnection(); + // Abre la conexión explícitamente si no se usa una transacción externa + if (connection is System.Data.Common.DbConnection dbConn && connection.State != ConnectionState.Open) + { + await dbConn.OpenAsync(); + } + else if (connection.State != ConnectionState.Open) + { + connection.Open(); + } + + using var transaction = connection.BeginTransaction(); + try + { + var creada = await _novedadRepository.CreateAsync(nuevaNovedad, idUsuario, transaction); + if (creada == null) + { + transaction.Rollback(); + return (null, "Error al guardar la novedad en la base de datos."); + } + + transaction.Commit(); + _logger.LogInformation("Novedad ID {IdNovedad} para Canilla ID {IdCanilla} creada por Usuario ID {UserId}.", creada.IdNovedad, creada.IdCanilla, idUsuario); + // Asegúrate que 'canilla.NomApe' sea accesible. Si GetByIdSimpleAsync devuelve la entidad Canilla, esto está bien. + return (MapToDto(creada, canilla.NomApe ?? "Canilla sin nombre"), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error durante Rollback en CrearAsync NovedadCanilla."); } + _logger.LogError(ex, "Error CrearAsync NovedadCanilla para Canilla ID: {IdCanilla}", createDto.IdCanilla); + return (null, $"Error interno al crear la novedad: {ex.Message}"); + } + finally + { + if (connection.State == ConnectionState.Open) connection.Close(); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNovedad, UpdateNovedadCanillaDto updateDto, int idUsuario) + { + var existente = await _novedadRepository.GetByIdAsync(idNovedad); + if (existente == null) + { + return (false, "Novedad no encontrada."); + } + + existente.Detalle = updateDto.Detalle; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn && connection.State != ConnectionState.Open) + { + await dbConn.OpenAsync(); + } + else if (connection.State != ConnectionState.Open) + { + connection.Open(); + } + using var transaction = connection.BeginTransaction(); + try + { + var actualizado = await _novedadRepository.UpdateAsync(existente, idUsuario, transaction); + if (!actualizado) + { + transaction.Rollback(); + return (false, "Error al actualizar la novedad en la base de datos."); + } + + transaction.Commit(); + _logger.LogInformation("Novedad ID {IdNovedad} actualizada por Usuario ID {UserId}.", idNovedad, idUsuario); + return (true, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error durante Rollback en ActualizarAsync NovedadCanilla."); } + _logger.LogError(ex, "Error ActualizarAsync NovedadCanilla ID: {IdNovedad}", idNovedad); + return (false, $"Error interno al actualizar la novedad: {ex.Message}"); + } + finally + { + if (connection.State == ConnectionState.Open) connection.Close(); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idNovedad, int idUsuario) + { + var existente = await _novedadRepository.GetByIdAsync(idNovedad); + if (existente == null) + { + return (false, "Novedad no encontrada."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn && connection.State != ConnectionState.Open) + { + await dbConn.OpenAsync(); + } + else if (connection.State != ConnectionState.Open) + { + connection.Open(); + } + using var transaction = connection.BeginTransaction(); + try + { + var eliminado = await _novedadRepository.DeleteAsync(idNovedad, idUsuario, transaction); + if (!eliminado) + { + transaction.Rollback(); + return (false, "Error al eliminar la novedad de la base de datos."); + } + transaction.Commit(); + _logger.LogInformation("Novedad ID {IdNovedad} eliminada por Usuario ID {UserId}.", idNovedad, idUsuario); + return (true, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error durante Rollback en EliminarAsync NovedadCanilla."); } + _logger.LogError(ex, "Error EliminarAsync NovedadCanilla ID: {IdNovedad}", idNovedad); + return (false, $"Error interno al eliminar la novedad: {ex.Message}"); + } + finally + { + if (connection.State == ConnectionState.Open) connection.Close(); + } + } + + public async Task> ObtenerReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta) + { + // Podría añadir validaciones o lógica de negocio adicional si fuera necesario + // antes de llamar al repositorio. Por ahora, es una llamada directa. + try + { + return await _novedadRepository.GetReporteNovedadesAsync(idEmpresa, fechaDesde, fechaHasta); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener datos para el reporte de novedades de canillitas. Empresa: {IdEmpresa}, Desde: {FechaDesde}, Hasta: {FechaHasta}", idEmpresa, fechaDesde, fechaHasta); + // Podría relanzar o devolver una lista vacía con un mensaje de error, + // dependiendo de cómo quiera manejar los errores en la capa de servicio. + // Por simplicidad, relanzamos para que el controlador lo maneje. + throw; + } + } + + public async Task> ObtenerReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta) + { + try + { + return await _novedadRepository.GetReporteGananciasAsync(idEmpresa, fechaDesde, fechaHasta); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener datos para el reporte de ganancias de canillitas. Empresa: {IdEmpresa}, Desde: {FechaDesde}, Hasta: {FechaHasta}", idEmpresa, fechaDesde, fechaHasta); + throw; + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs b/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs index 3a84099..3cb1e44 100644 --- a/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs +++ b/Backend/GestionIntegral.Api/Services/Reportes/IReportesService.cs @@ -69,5 +69,8 @@ namespace GestionIntegral.Api.Services.Reportes IEnumerable Ganancias, string? Error )> ObtenerDatosTicketLiquidacionAsync(DateTime fecha, int idCanilla); + + Task<(IEnumerable Data, string? Error)> ObtenerReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); + Task<(IEnumerable Data, string? Error)> ObtenerReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs b/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs index 38f0cb5..0df9c16 100644 --- a/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs +++ b/Backend/GestionIntegral.Api/Services/Reportes/ReportesService.cs @@ -490,5 +490,33 @@ namespace GestionIntegral.Api.Services.Reportes ); } } + + public async Task<(IEnumerable Data, string? Error)> ObtenerReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista) + { + try + { + var data = await _reportesRepository.GetReporteMensualDiariosAsync(fechaDesde, fechaHasta, esAccionista); + return (data, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener reporte mensual canillitas (diarios)."); + return (Enumerable.Empty(), "Error al obtener datos del reporte (diarios)."); + } + } + + public async Task<(IEnumerable Data, string? Error)> ObtenerReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista) + { + try + { + var data = await _reportesRepository.GetReporteMensualPorPublicacionAsync(fechaDesde, fechaHasta, esAccionista); + return (data, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener reporte mensual canillitas (por publicación)."); + return (Enumerable.Empty(), "Error al obtener datos del reporte (por publicación)."); + } + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs index b742cf0..d9225ca 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs @@ -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+062cc05fd00a484e43f8b4ff022e53ac49670a78")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8fb94f8cefc3b498397ffcbb9b9a2e66c13b25b9")] [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json index 5beb38e..408a79b 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","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=","1I2C2FVhJyFRbvyuGXnropbTYN\u002BqpCoTcHfxWbfWF10="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","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=","ko3Qcj1qg4o0KikPIBL6WHcUA8sCBGtBIyzr8DuluqQ="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json index e25e282..c2be6f1 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","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=","1I2C2FVhJyFRbvyuGXnropbTYN\u002BqpCoTcHfxWbfWF10="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","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=","ko3Qcj1qg4o0KikPIBL6WHcUA8sCBGtBIyzr8DuluqQ="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx b/Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx new file mode 100644 index 0000000..fa05d9a --- /dev/null +++ b/Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, InputAdornment +} from '@mui/material'; +import type { SaldoGestionDto } from '../../../models/dtos/Contables/SaldoGestionDto'; +import type { AjusteSaldoRequestDto } from '../../../models/dtos/Contables/AjusteSaldoRequestDto'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 450 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 3, +}; + +interface AjusteSaldoModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: AjusteSaldoRequestDto) => Promise; // El padre maneja la recarga + saldoParaAjustar: SaldoGestionDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const AjusteSaldoModal: React.FC = ({ + open, + onClose, + onSubmit, + saldoParaAjustar, + errorMessage, + clearErrorMessage +}) => { + const [montoAjuste, setMontoAjuste] = useState(''); + const [justificacion, setJustificacion] = useState(''); + const [loading, setLoading] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + useEffect(() => { + if (open) { + setMontoAjuste(''); + setJustificacion(''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + const numMontoAjuste = parseFloat(montoAjuste); + + if (!montoAjuste.trim()) { + errors.montoAjuste = 'El monto de ajuste es obligatorio.'; + } else if (isNaN(numMontoAjuste)) { + errors.montoAjuste = 'El monto debe ser un número.'; + } else if (numMontoAjuste === 0) { + errors.montoAjuste = 'El monto de ajuste no puede ser cero.'; + } + + if (!justificacion.trim()) { + errors.justificacion = 'La justificación es obligatoria.'; + } else if (justificacion.trim().length < 5 || justificacion.trim().length > 250) { + errors.justificacion = 'La justificación debe tener entre 5 y 250 caracteres.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion') => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate() || !saldoParaAjustar) return; + + setLoading(true); + try { + const dataToSubmit: AjusteSaldoRequestDto = { + destino: saldoParaAjustar.destino as 'Distribuidores' | 'Canillas', + idDestino: saldoParaAjustar.idDestino, + idEmpresa: saldoParaAjustar.idEmpresa, + montoAjuste: parseFloat(montoAjuste), + justificacion, + }; + await onSubmit(dataToSubmit); + onClose(); // Cerrar en éxito (el padre recargará) + } catch (error: any) { + // El error de API es manejado por la página padre + console.error("Error en submit de AjusteSaldoModal:", error); + } finally { + setLoading(false); + } + }; + + if (!saldoParaAjustar) return null; + + return ( + + + + Ajustar Saldo Manualmente + + + Destinatario: {saldoParaAjustar.nombreDestinatario} ({saldoParaAjustar.destino}) + + + Empresa: {saldoParaAjustar.nombreEmpresa} + + + Saldo Actual: {saldoParaAjustar.monto.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })} + + + + { setMontoAjuste(e.target.value); handleInputChange('montoAjuste'); }} + margin="normal" + error={!!localErrors.montoAjuste} + helperText={localErrors.montoAjuste || 'Ingrese un valor positivo para aumentar deuda o negativo para disminuirla.'} + disabled={loading} + InputProps={{ startAdornment: $ }} + inputProps={{ step: "0.01" }} + autoFocus + /> + { setJustificacion(e.target.value); handleInputChange('justificacion'); }} + margin="normal" + multiline + rows={3} + error={!!localErrors.justificacion} + helperText={localErrors.justificacion || ''} + disabled={loading} + inputProps={{ maxLength: 250 }} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default AjusteSaldoModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx index 24e5f8a..5ea581f 100644 --- a/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx +++ b/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx @@ -8,14 +8,14 @@ import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; -import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; -import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; +// import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Ya no es necesario cargar todos los canillitas aquí import publicacionService from '../../../services/Distribucion/publicacionService'; -import canillaService from '../../../services/Distribucion/canillaService'; +// import canillaService from '../../../services/Distribucion/canillaService'; // Ya no es necesario import entradaSalidaCanillaService from '../../../services/Distribucion/entradaSalidaCanillaService'; import type { CreateBulkEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto'; import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaItemDto'; import axios from 'axios'; +import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto'; const modalStyle = { position: 'absolute' as 'absolute', @@ -34,14 +34,21 @@ const modalStyle = { interface EntradaSalidaCanillaFormModalProps { open: boolean; onClose: () => void; + // El onSubmit de la página padre se usa solo para edición. La creación se maneja internamente. onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise; - initialData?: EntradaSalidaCanillaDto | null; + initialData?: EntradaSalidaCanillaDto | null; // Para edición + prefillData?: { // Para creación, prellenar desde la página padre + fecha?: string; // YYYY-MM-DD + idCanilla?: number | string; + nombreCanilla?: string; // << AÑADIR NOMBRE PARA MOSTRAR + idPublicacion?: number | string; // Para pre-seleccionar la primera publicación en la lista de items + } | null; errorMessage?: string | null; clearErrorMessage: () => void; } interface FormRowItem { - id: string; + id: string; // ID temporal para el frontend idPublicacion: number | string; cantSalida: string; cantEntrada: string; @@ -50,56 +57,62 @@ interface FormRowItem { const EntradaSalidaCanillaFormModal: React.FC = ({ open, - onClose, // Este onClose es el que se pasa desde GestionarEntradasSalidasCanillaPage - onSubmit, // Este onSubmit es el que se pasa para la lógica de EDICIÓN + onClose, + onSubmit: onSubmitEdit, // Renombrar para claridad, ya que solo se usa para editar initialData, + prefillData, errorMessage: parentErrorMessage, clearErrorMessage }) => { - const [idCanilla, setIdCanilla] = useState(''); - const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); - const [editIdPublicacion, setEditIdPublicacion] = useState(''); + // Estados para los campos que SÍ son editables o parte del formulario de items + const [editIdPublicacion, setEditIdPublicacion] = useState(''); // Solo para modo edición const [editCantSalida, setEditCantSalida] = useState('0'); const [editCantEntrada, setEditCantEntrada] = useState('0'); const [editObservacion, setEditObservacion] = useState(''); - const [items, setItems] = useState([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); // Iniciar con una fila - const [publicaciones, setPublicaciones] = useState([]); - const [canillitas, setCanillitas] = useState([]); - const [loading, setLoading] = useState(false); // Loading para submit - const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs - const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items + const [items, setItems] = useState([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); + + // Estados para datos de dropdowns + const [publicaciones, setPublicaciones] = useState([]); // Sigue siendo necesario para la lista de items + + // Estados de carga y error + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Solo para publicaciones + const [loadingItems, setLoadingItems] = useState(false); const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); const [modalSpecificApiError, setModalSpecificApiError] = useState(null); - const isEditing = Boolean(initialData); + const isEditing = Boolean(initialData && initialData.idParte); - // Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true + // Datos que vienen prellenados y no son editables en el modal (Fecha y Canillita) + const displayFecha = isEditing ? (initialData?.fecha ? initialData.fecha.split('T')[0] : '') : (prefillData?.fecha || ''); + const displayIdCanilla = isEditing ? initialData?.idCanilla : prefillData?.idCanilla; + const displayNombreCanilla = isEditing ? initialData?.nomApeCanilla : prefillData?.nombreCanilla; + + + // Cargar publicaciones para el dropdown de items useEffect(() => { - const fetchDropdownData = async () => { + const fetchPublicacionesDropdown = async () => { setLoadingDropdowns(true); setLocalErrors(prev => ({ ...prev, dropdowns: null })); try { - const [pubsData, canillitasData] = await Promise.all([ - publicacionService.getAllPublicaciones(undefined, undefined, true), - canillaService.getAllCanillas(undefined, undefined, true) - ]); + // Usar getPublicacionesForDropdown si lo tienes, sino getAllPublicaciones + const pubsData = await publicacionService.getPublicacionesForDropdown(true); setPublicaciones(pubsData); - setCanillitas(canillitasData); } catch (error) { - console.error("Error al cargar datos para dropdowns", error); - setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios (publicaciones/canillitas).' })); + console.error("Error al cargar publicaciones para dropdown", error); + setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' })); } finally { setLoadingDropdowns(false); } }; if (open) { - fetchDropdownData(); + fetchPublicacionesDropdown(); } }, [open]); - // Efecto para inicializar el formulario cuando se abre o cambia initialData + // Inicializar formulario y/o pre-cargar items useEffect(() => { if (open) { clearErrorMessage(); @@ -107,65 +120,90 @@ const EntradaSalidaCanillaFormModal: React.FC 0) { + // Si ya tenemos publicaciones, y un prefill de publicación, intentamos setearlo + const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay(); + setLoadingItems(true); + publicacionService.getPublicacionesPorDiaSemana(diaSemana) + .then(pubsPorDefecto => { + let itemsIniciales: FormRowItem[]; + if (pubsPorDefecto.find(p => p.idPublicacion === Number(idPubPrefill))) { + // Si la publicación prellenada está en las de por defecto, la usamos + itemsIniciales = [{ id: Date.now().toString(), idPublicacion: Number(idPubPrefill), cantSalida: '0', cantEntrada: '0', observacion: '' }]; + } else if (pubsPorDefecto.length > 0) { + // Si no, pero hay otras por defecto, usamos la primera de ellas + itemsIniciales = pubsPorDefecto.map(pub => ({ + id: `${Date.now().toString()}-${pub.idPublicacion}`, + idPublicacion: pub.idPublicacion, + cantSalida: '0', cantEntrada: '0', observacion: '' + })); + } else { + // Si no hay ninguna por defecto, y la prellenada no aplica, usamos la prellenada sola o vacía + itemsIniciales = [{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }]; + } + setItems(itemsIniciales.length > 0 ? itemsIniciales : [{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); + }) + .catch(() => setItems([{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }])) // Fallback + .finally(() => setLoadingItems(false)); + } else if (publicaciones.length === 0 && !loadingDropdowns) { // Si no hay prefill de pub o no hay pubs aún + setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); + } } } - }, [open, initialData, isEditing, clearErrorMessage]); + }, [open, initialData, isEditing, prefillData, clearErrorMessage, publicaciones, loadingDropdowns, displayFecha]); // Añadir displayFecha - - // Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO) - // y cuando las publicaciones están disponibles. + // Efecto para pre-cargar items por defecto cuando cambia la FECHA (displayFecha) en modo NUEVO useEffect(() => { - if (open && !isEditing && publicaciones.length > 0 && fecha) { // Asegurarse que 'fecha' tiene un valor - const diaSemana = new Date(fecha + 'T00:00:00Z').getUTCDay(); // Usar UTC para getDay consistente - setLoadingItems(true); // Indicador de carga para los items - setLocalErrors(prev => ({ ...prev, general: null })); - + if (open && !isEditing && publicaciones.length > 0 && displayFecha) { + const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay(); + setLoadingItems(true); publicacionService.getPublicacionesPorDiaSemana(diaSemana) .then(pubsPorDefecto => { - if (pubsPorDefecto.length > 0) { - const itemsPorDefecto = pubsPorDefecto.map(pub => ({ - id: `${Date.now().toString()}-${pub.idPublicacion}`, - idPublicacion: pub.idPublicacion, - cantSalida: '0', - cantEntrada: '0', - observacion: '' - })); - setItems(itemsPorDefecto); - } else { - // Si no hay configuraciones para el día, iniciar con una fila vacía - setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); - } + const itemsPorDefecto = pubsPorDefecto.map(pub => ({ + id: `${Date.now().toString()}-${pub.idPublicacion}`, + idPublicacion: pub.idPublicacion, + cantSalida: '0', + cantEntrada: '0', + observacion: '' + })); + setItems(itemsPorDefecto.length > 0 ? itemsPorDefecto : [{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); }) .catch(err => { - console.error("Error al cargar/recargar publicaciones por defecto para el día:", err); + console.error("Error al cargar publicaciones por defecto para el día:", err); setLocalErrors(prev => ({ ...prev, general: 'Error al pre-cargar publicaciones del día.' })); setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); }) .finally(() => setLoadingItems(false)); - } else if (open && !isEditing && publicaciones.length === 0 && !loadingDropdowns) { - // Si las publicaciones aún no se cargaron pero los dropdowns terminaron de cargar, iniciar con 1 item vacío. - setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); } - }, [open, isEditing, fecha, publicaciones, loadingDropdowns]); // Dependencias clave + }, [open, isEditing, displayFecha, publicaciones]); // Dependencia de displayFecha y publicaciones + const validate = (): boolean => { + // ... (lógica de validación sin cambios, pero 'idCanilla' y 'fecha' ya no son estados del modal) const currentErrors: { [key: string]: string | null } = {}; - if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.'; - if (!fecha.trim()) currentErrors.fecha = 'La fecha es obligatoria.'; - else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; - + // Validar displayIdCanilla y displayFecha si es modo creación + if (!isEditing) { + if (!displayIdCanilla) currentErrors.idCanilla = 'El canillita es obligatorio (provisto por la página).'; + if (!displayFecha || !displayFecha.trim()) currentErrors.fecha = 'La fecha es obligatoria (provista por la página).'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(displayFecha)) currentErrors.fecha = 'Formato de fecha inválido.'; + } + // ... resto de la validación para items (modo creación) o campos edit (modo edición) ... if (isEditing) { const salidaNum = parseInt(editCantSalida, 10); const entradaNum = parseInt(editCantEntrada, 10); @@ -177,14 +215,15 @@ const EntradaSalidaCanillaFormModal: React.FC salidaNum) { currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; } - } else { + if (!editIdPublicacion) { // En edición, la publicación es fija, pero debe existir + currentErrors.editIdPublicacion = 'Error: Publicación no especificada para edición.'; + } + } else { // Modo Creación (Bulk) let hasValidItemWithQuantityOrPub = false; const publicacionIdsEnLote = new Set(); - if (items.length === 0) { currentErrors.general = "Debe agregar al menos una publicación."; } - items.forEach((item, index) => { const salidaNum = parseInt(item.cantSalida, 10); const entradaNum = parseInt(item.cantEntrada, 10); @@ -199,9 +238,8 @@ const EntradaSalidaCanillaFormModal: React.FC itm.idPublicacion === '' && - (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && - (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && - itm.observacion.trim() === '' - ); - - if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { + const allItemsAreEmptyAndNoPubSelected = items.every(itm => itm.idPublicacion === '' && (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && itm.observacion.trim() === ''); + if (!hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones."; - } else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0)) && !allItemsAreEmptyAndNoPubSelected) { - currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos."; + } else if (items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0 || i.observacion.trim() !== '')) && !allItemsAreEmptyAndNoPubSelected) { + currentErrors.general = "Debe ingresar datos significativos (cantidades u observación) para al menos una publicación seleccionada."; } } setLocalErrors(currentErrors); @@ -232,6 +263,7 @@ const EntradaSalidaCanillaFormModal: React.FC { + // ... (sin cambios) if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); if (parentErrorMessage) clearErrorMessage(); if (modalSpecificApiError) setModalSpecificApiError(null); @@ -252,16 +284,17 @@ const EntradaSalidaCanillaFormModal: React.FC item.idPublicacion && Number(item.idPublicacion) > 0 && - ((parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '') + ( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '' ) ) .map(item => ({ idPublicacion: Number(item.idPublicacion), @@ -271,37 +304,30 @@ const EntradaSalidaCanillaFormModal: React.FC ({ ...prev, general: "No hay movimientos válidos para registrar..." })); + setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar." })); setLoading(false); return; } - const bulkData: CreateBulkEntradaSalidaCanillaDto = { - idCanilla: Number(idCanilla), - fecha, + idCanilla: Number(displayIdCanilla), // Usar el displayIdCanilla + fecha: displayFecha, // Usar el displayFecha items: itemsToSubmit, }; await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData); - onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso } - // onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito + onClose(); } catch (error: any) { console.error("Error en submit de EntradaSalidaCanillaFormModal:", error); - if (axios.isAxiosError(error) && error.response) { - setModalSpecificApiError(error.response.data?.message || 'Error al procesar la solicitud.'); - } else { - setModalSpecificApiError('Ocurrió un error inesperado.'); - } - // NO llamar a onClose() aquí si hubo un error, para que el modal permanezca abierto - // y muestre el modalSpecificApiError. - // Si la edición (que usa el 'onSubmit' del padre) lanza un error, ese error se propagará - // al padre y el padre decidirá si el modal se cierra o no (actualmente no lo cierra). + const message = axios.isAxiosError(error) && error.response?.data?.message + ? error.response.data.message + : 'Ocurrió un error inesperado al procesar la solicitud.'; + setModalSpecificApiError(message); } finally { setLoading(false); } }; - const handleAddRow = () => { + const handleAddRow = () => { /* ... (sin cambios) ... */ if (items.length >= publicaciones.length) { setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); return; @@ -309,15 +335,13 @@ const EntradaSalidaCanillaFormModal: React.FC ({ ...prev, general: null })); }; - - const handleRemoveRow = (idToRemove: string) => { + const handleRemoveRow = (idToRemove: string) => { /* ... (sin cambios) ... */ if (items.length <= 1 && !isEditing) return; setItems(items.filter(item => item.id !== idToRemove)); }; - - const handleItemChange = (id: string, field: keyof Omit, value: string | number) => { - setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); // CORREGIDO: item a itemRow para evitar conflicto de nombres de variable con el `item` del map en el JSX - if (localErrors[`item_${id}_${field}`]) { // Aquí item se refiere al id del item. + const handleItemChange = (id: string, field: keyof Omit, value: string | number) => { /* ... (sin cambios) ... */ + setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); + if (localErrors[`item_${id}_${field}`]) { setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null })); } if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null })); @@ -325,200 +349,102 @@ const EntradaSalidaCanillaFormModal: React.FC - {isEditing ? 'Editar Movimiento Canillita' : 'Registrar Movimientos Canillita'} + {isEditing ? `Editar Movimiento (ID: ${initialData?.idParte})` : 'Registrar Nuevos Movimientos'} - - - Canillita - - {localErrors.idCanilla && {localErrors.idCanilla}} - + {/* --- MOSTRAR DATOS PRELLENADOS/FIJOS --- */} + + + {isEditing ? "Canillita:" : "Para Canillita:"} {displayNombreCanilla || 'N/A'} + {isEditing ? "Fecha Movimiento:" : "Para Fecha:"} {displayFecha ? new Date(displayFecha + 'T00:00:00Z').toLocaleDateString('es-AR', {timeZone: 'UTC'}) : 'N/A'} + + {localErrors.idCanilla && {localErrors.idCanilla}} + {localErrors.fecha && {localErrors.fecha}} + + {/* --- FIN DATOS PRELLENADOS --- */} - { setFecha(e.target.value); handleInputChange('fecha'); }} - margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} - disabled={loading || isEditing} InputLabelProps={{ shrink: true }} - autoFocus={!isEditing && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado - /> + {/* El Select de Canillita y TextField de Fecha se eliminan de aquí si son fijos */} - {isEditing && initialData && ( - - Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`} - - { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }} - margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} - disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }} - /> - { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }} - margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} - disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }} - /> - - setEditObservacion(e.target.value)} - margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }} + {isEditing && initialData && ( + + + Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`} + + + { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }} + margin="dense" error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} + disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }} /> - - )} + { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }} + margin="dense" error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} + disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }} + /> + + setEditObservacion(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }} + /> + + )} - {!isEditing && ( - - Movimientos por Publicación: - {/* Indicador de carga para los items */} - {loadingItems && } - {!loadingItems && items.map((itemRow, index) => ( - - {/* Nivel 1: contenedor “padre” sin wrap */} - - {/* Nivel 2: contenedor que agrupa solo los campos y sí puede hacer wrap */} - - - 0 || - parseInt(itemRow.cantEntrada) > 0 || - itemRow.observacion.trim() !== '' - } - > + {!isEditing && ( + + Movimientos por Publicación: + {loadingItems && } + {!loadingItems && items.map((itemRow, index) => ( + // ... (Renderizado de la fila de items sin cambios significativos, + // solo asegúrate que el Select de Publicación use `publicaciones` y no `canillitas`) + + + + + 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== '' } > Pub. {index + 1} - handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)} + disabled={loading || loadingDropdowns} sx={{ minWidth: 0 }} > + Seleccione + {publicaciones.map((p) => ( {p.nombre} ))} - {localErrors[`item_${itemRow.id}_idPublicacion`] && ( - - {localErrors[`item_${itemRow.id}_idPublicacion`]} - - )} + {localErrors[`item_${itemRow.id}_idPublicacion`] && ( {localErrors[`item_${itemRow.id}_idPublicacion`]} )} - - handleItemChange(itemRow.id, 'cantSalida', e.target.value)} - error={!!localErrors[`item_${itemRow.id}_cantSalida`]} - helperText={localErrors[`item_${itemRow.id}_cantSalida`]} - inputProps={{ min: 0 }} - sx={{ - flexBasis: 'calc(15% - 8px)', - minWidth: '80px', - minHeight: 0, - }} + error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]} + inputProps={{ min: 0 }} sx={{ flexBasis: 'calc(15% - 8px)', minWidth: '80px', minHeight: 0, }} /> - - handleItemChange(itemRow.id, 'cantEntrada', e.target.value)} - error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} - helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} - inputProps={{ min: 0 }} - sx={{ - flexBasis: 'calc(15% - 8px)', - minWidth: '80px', - minHeight: 0, - }} + error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} + inputProps={{ min: 0 }} sx={{ flexBasis: 'calc(15% - 8px)', minWidth: '80px', minHeight: 0, }} /> - - handleItemChange(itemRow.id, 'observacion', e.target.value)} - size="small" - sx={{ - flexGrow: 1, - flexBasis: 'calc(25% - 8px)', - minWidth: '120px', - minHeight: 0, - }} - multiline - maxRows={1} + handleItemChange(itemRow.id, 'observacion', e.target.value)} + size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px', minHeight: 0, }} + multiline maxRows={1} /> - - {/* Ícono de eliminar: siempre en la misma línea */} {items.length > 1 && ( - handleRemoveRow(itemRow.id)} - color="error" - aria-label="Quitar fila" - sx={{ - alignSelf: 'center', // mantén centrado verticalmente - // No necesita flexShrink, porque el padre no hace wrap - }} - > + handleRemoveRow(itemRow.id)} color="error" aria-label="Quitar fila" sx={{ alignSelf: 'center', }} > )} - ))} - - {localErrors.general && {localErrors.general}} - - - )} - + ))} + {localErrors.general && {localErrors.general}} + + + )} {parentErrorMessage && {parentErrorMessage}} {modalSpecificApiError && {modalSpecificApiError}} diff --git a/Frontend/src/components/Modals/Distribucion/NovedadCanillaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/NovedadCanillaFormModal.tsx new file mode 100644 index 0000000..faa55df --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/NovedadCanillaFormModal.tsx @@ -0,0 +1,165 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert +} from '@mui/material'; +import type { NovedadCanillaDto } from '../../../models/dtos/Distribucion/NovedadCanillaDto'; +import type { CreateNovedadCanillaDto } from '../../../models/dtos/Distribucion/CreateNovedadCanillaDto'; +import type { UpdateNovedadCanillaDto } from '../../../models/dtos/Distribucion/UpdateNovedadCanillaDto'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 500 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface NovedadCanillaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => Promise; + // Props para pasar datos necesarios: + idCanilla: number | null; // Necesario para crear una nueva novedad + nombreCanilla?: string; // Para mostrar en el título + initialData?: NovedadCanillaDto | null; // Para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const NovedadCanillaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + idCanilla, + nombreCanilla, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [fecha, setFecha] = useState(''); + const [detalle, setDetalle] = useState(''); + const [loading, setLoading] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData && initialData.idNovedad); + + useEffect(() => { + if (open) { + setFecha(initialData?.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]); + setDetalle(initialData?.detalle || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; + + if (!detalle.trim()) errors.detalle = 'El detalle es obligatorio.'; + else if (detalle.trim().length > 250) errors.detalle = 'El detalle no puede exceder los 250 caracteres.'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: 'fecha' | 'detalle') => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + if (isEditing && initialData) { + const dataToSubmit: UpdateNovedadCanillaDto = { detalle }; + await onSubmit(dataToSubmit, initialData.idNovedad); + } else if (idCanilla) { // Asegurarse que idCanilla esté disponible para creación + const dataToSubmit: CreateNovedadCanillaDto = { + idCanilla: idCanilla, // Tomado de props + fecha, + detalle, + }; + await onSubmit(dataToSubmit); + } else { + // Esto no debería pasar si la lógica de la página que llama al modal es correcta + setLocalErrors(prev => ({...prev, general: "No se proporcionó ID de Canillita para crear la novedad."})) + setLoading(false); + return; + } + onClose(); + } catch (error: any) { + // El error de API es manejado por la página padre a través de 'errorMessage' + console.error("Error en submit de NovedadCanillaFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Novedad' : `Agregar Novedad para ${nombreCanilla || 'Canillita'}`} + + + {isEditing && initialData ? `Editando Novedad ID: ${initialData.idNovedad}` : `Canillita ID: ${idCanilla || 'N/A'}`} + + + {setFecha(e.target.value); handleInputChange('fecha');}} + margin="normal" + fullWidth + error={!!localErrors.fecha} + helperText={localErrors.fecha || ''} + disabled={loading || isEditing} // Fecha no se edita + InputLabelProps={{ shrink: true }} + autoFocus={!isEditing} + /> + {setDetalle(e.target.value); handleInputChange('detalle');}} + margin="normal" + fullWidth + multiline + rows={4} + error={!!localErrors.detalle} + helperText={localErrors.detalle || (detalle ? `${250 - detalle.length} caracteres restantes` : '')} + disabled={loading} + inputProps={{ maxLength: 250 }} + /> + + {errorMessage && {errorMessage}} + {localErrors.general && {localErrors.general}} + + + + + + + + + + ); +}; + +export default NovedadCanillaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx b/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx index bb55653..31232ad 100644 --- a/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx +++ b/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx @@ -1,67 +1,225 @@ -import React from 'react'; -import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper } from '@mui/material'; // Quitar Grid +import React, { useState } from 'react'; +import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper, Divider, TextField } from '@mui/material'; import type { PermisoAsignadoDto } from '../../../models/dtos/Usuarios/PermisoAsignadoDto'; interface PermisosChecklistProps { permisosDisponibles: PermisoAsignadoDto[]; permisosSeleccionados: Set; - onPermisoChange: (permisoId: number, asignado: boolean) => void; + onPermisoChange: (permisoId: number, asignado: boolean, esPermisoSeccion?: boolean, moduloHijo?: string) => void; disabled?: boolean; } +const SECCION_PERMISSIONS_PREFIX = "SS"; + +// Mapeo de codAcc de sección a su módulo conceptual +const getModuloFromSeccionCodAcc = (codAcc: string): string | null => { + if (codAcc === "SS001") return "Distribución"; + if (codAcc === "SS002") return "Contables"; + if (codAcc === "SS003") return "Impresión"; + if (codAcc === "SS004") return "Reportes"; + if (codAcc === "SS005") return "Radios"; + if (codAcc === "SS006") return "Usuarios"; + return null; +}; + +// Función para determinar el módulo conceptual de un permiso individual +const getModuloConceptualDelPermiso = (permisoModulo: string): string => { + const moduloLower = permisoModulo.toLowerCase(); + + if (moduloLower.includes("distribuidores") || + moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas" + moduloLower.includes("publicaciones distribución") || + moduloLower.includes("zonas distribuidores") || + moduloLower.includes("movimientos distribuidores") || + moduloLower.includes("empresas") || // Módulo "Empresas" + moduloLower.includes("otros destinos") || // Cubre "Otros Destinos" y "Salidas Otros Destinos" + moduloLower.includes("ctrl. devoluciones")) { + return "Distribución"; + } + if (moduloLower.includes("cuentas pagos") || + moduloLower.includes("cuentas notas") || + moduloLower.includes("cuentas tipos pagos")) { + return "Contables"; + } + if (moduloLower.includes("impresión tiradas") || + moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas" + moduloLower.includes("impresión plantas") || + moduloLower.includes("tipos bobinas")) { // Añadido explícitamente + return "Impresión"; + } + if (moduloLower.includes("radios")) { // Asumiendo que los permisos de radios tendrán "Radios" en su módulo + return "Radios"; + } + if (moduloLower.includes("usuarios") || // Cubre "Usuarios" y "Perfiles" + moduloLower.includes("perfiles")) { + return "Usuarios"; + } + if (moduloLower.includes("reportes")) { // Para los permisos RRxxx + return "Reportes"; + } + if (moduloLower.includes("permisos")) { // Para "Permisos (Definición)" + return "Permisos (Definición)"; + } + return permisoModulo; // Fallback al nombre original si no coincide +}; + + const PermisosChecklist: React.FC = ({ permisosDisponibles, permisosSeleccionados, onPermisoChange, disabled = false, }) => { - const permisosAgrupados = permisosDisponibles.reduce((acc, permiso) => { - const modulo = permiso.modulo || 'Otros'; - if (!acc[modulo]) { - acc[modulo] = []; + const [filtrosModulo, setFiltrosModulo] = useState>({}); + + const handleFiltroChange = (moduloConceptual: string, texto: string) => { + setFiltrosModulo(prev => ({ ...prev, [moduloConceptual]: texto.toLowerCase() })); + }; + + const permisosDeSeccion = permisosDisponibles.filter(p => p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX)); + const permisosNormales = permisosDisponibles.filter(p => !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX)); + + const permisosAgrupadosConceptualmente = permisosNormales.reduce((acc, permiso) => { + const moduloConceptual = getModuloConceptualDelPermiso(permiso.modulo); + if (!acc[moduloConceptual]) { + acc[moduloConceptual] = []; } - acc[modulo].push(permiso); + acc[moduloConceptual].push(permiso); return acc; }, {} as Record); + const ordenModulosPrincipales = ["Distribución", "Contables", "Impresión", "Radios", "Usuarios", "Reportes", "Permisos (Definición)"]; + // Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún) + permisosDeSeccion.forEach(ps => { + const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc); + if (moduloConceptual && !ordenModulosPrincipales.includes(moduloConceptual)) { + // Insertar después de un módulo conocido o al final si no hay un orden específico para él + const indexReportes = ordenModulosPrincipales.indexOf("Reportes"); + if (indexReportes !== -1) { + ordenModulosPrincipales.splice(indexReportes, 0, moduloConceptual); + } else { + ordenModulosPrincipales.push(moduloConceptual); + } + } + if (moduloConceptual && !permisosAgrupadosConceptualmente[moduloConceptual]) { + permisosAgrupadosConceptualmente[moduloConceptual] = []; // Asegurar que el grupo exista para Radios + } + }); + // Eliminar duplicados del orden por si acaso + const ordenFinalModulos = [...new Set(ordenModulosPrincipales)]; + + return ( - {/* Contenedor Flexbox */} - {Object.entries(permisosAgrupados).map(([modulo, permisosDelModulo]) => ( - - - - {modulo} - - {/* Para que ocupe el espacio vertical */} - {permisosDelModulo.map((permiso) => ( - onPermisoChange(permiso.id, e.target.checked)} - disabled={disabled} - size="small" - /> - } - label={{`${permiso.descPermiso} (${permiso.codAcc})`}} + + {ordenFinalModulos.map(moduloConceptual => { // Usar ordenFinalModulos + const permisoSeccionAsociado = permisosDeSeccion.find( + ps => getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptual + ); + const permisosDelModuloHijosOriginales = permisosAgrupadosConceptualmente[moduloConceptual] || []; + + // Condición para renderizar la sección + if (!permisoSeccionAsociado && permisosDelModuloHijosOriginales.length === 0 && moduloConceptual !== "Permisos (Definición)") { + // No renderizar si no hay permiso de sección Y no hay hijos, EXCEPTO para "Permisos (Definición)" que es especial + return null; + } + if (moduloConceptual === "Permisos (Definición)" && permisosDelModuloHijosOriginales.length === 0) { + // No renderizar "Permisos (Definición)" si no tiene hijos + return null; + } + + + const esSeccionSeleccionada = permisoSeccionAsociado ? permisosSeleccionados.has(permisoSeccionAsociado.id) : false; + const todosHijosSeleccionados = permisosDelModuloHijosOriginales.length > 0 && permisosDelModuloHijosOriginales.every(p => permisosSeleccionados.has(p.id)); + const ningunHijoSeleccionado = permisosDelModuloHijosOriginales.every(p => !permisosSeleccionados.has(p.id)); + const algunosHijosSeleccionados = !todosHijosSeleccionados && !ningunHijoSeleccionado && permisosDelModuloHijosOriginales.length > 0; + + const textoFiltro = filtrosModulo[moduloConceptual] || ''; + const permisosDelModuloHijosFiltrados = textoFiltro + ? permisosDelModuloHijosOriginales.filter(permiso => + permiso.descPermiso.toLowerCase().includes(textoFiltro) || + permiso.codAcc.toLowerCase().includes(textoFiltro) + ) + : permisosDelModuloHijosOriginales; + + return ( + + + + {moduloConceptual} + + + {permisosDelModuloHijosOriginales.length > 3 && ( // Mostrar filtro si hay más de 3 permisos + handleFiltroChange(moduloConceptual, e.target.value)} + sx={{ mb: 1, mt: 0.5 }} + disabled={disabled} /> - ))} - - - - ))} + )} + + {permisoSeccionAsociado && ( + <> + 0))} + onChange={() => onPermisoChange(permisoSeccionAsociado.id, false, true, moduloConceptual)} + disabled={disabled} + size="small" + /> + } + /> + {/* Mostrar Divider solo si hay hijos o es la sección Radios (que queremos que aparezca aunque esté vacía) */} + {(permisosDelModuloHijosOriginales.length > 0 || moduloConceptual === "Radios") && } + + )} + + {/* Aumentar un poco maxHeight */} + + {permisosDelModuloHijosFiltrados.map((permiso) => ( + onPermisoChange(permiso.id, e.target.checked, false, moduloConceptual)} + disabled={disabled || (permisoSeccionAsociado && !esSeccionSeleccionada)} + size="small" + /> + } + label={{`${permiso.descPermiso} (${permiso.codAcc})`}} + /> + ))} + {textoFiltro && permisosDelModuloHijosFiltrados.length === 0 && permisosDelModuloHijosOriginales.length > 0 && ( + + No hay permisos que coincidan con "{textoFiltro}". + + )} + {/* Mensaje si no hay hijos en general (y no es por filtro) */} + {permisosDelModuloHijosOriginales.length === 0 && !textoFiltro && moduloConceptual !== "Permisos (Definición)" && ( + + No hay permisos específicos en esta sección. + + )} + + + + + ); + })} ); }; diff --git a/Frontend/src/layouts/MainLayout.tsx b/Frontend/src/layouts/MainLayout.tsx index eef07a6..5382c65 100644 --- a/Frontend/src/layouts/MainLayout.tsx +++ b/Frontend/src/layouts/MainLayout.tsx @@ -1,32 +1,37 @@ -import React, { type ReactNode, useState, useEffect } from 'react'; +// src/layouts/MainLayout.tsx +import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo + from 'react'; import { Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper, - IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider // Nuevas importaciones + IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider, + Button } from '@mui/material'; -import AccountCircle from '@mui/icons-material/AccountCircle'; // Icono de usuario -import LockResetIcon from '@mui/icons-material/LockReset'; // Icono para cambiar contraseña -import LogoutIcon from '@mui/icons-material/Logout'; // Icono para cerrar sesión +import AccountCircle from '@mui/icons-material/AccountCircle'; +import LockResetIcon from '@mui/icons-material/LockReset'; +import LogoutIcon from '@mui/icons-material/Logout'; import { useAuth } from '../contexts/AuthContext'; import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; import { useNavigate, useLocation } from 'react-router-dom'; +import { usePermissions } from '../hooks/usePermissions'; // <<--- AÑADIR ESTA LÍNEA interface MainLayoutProps { children: ReactNode; } -const modules = [ - { label: 'Inicio', path: '/' }, - { label: 'Distribución', path: '/distribucion' }, - { label: 'Contables', path: '/contables' }, - { label: 'Impresión', path: '/impresion' }, - { label: 'Reportes', path: '/reportes' }, - { label: 'Radios', path: '/radios' }, - { label: 'Usuarios', path: '/usuarios' }, +// Definición original de módulos +const allAppModules = [ + { label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible + { label: 'Distribución', path: '/distribucion', requiredPermission: 'SS001' }, + { label: 'Contables', path: '/contables', requiredPermission: 'SS002' }, + { label: 'Impresión', path: '/impresion', requiredPermission: 'SS003' }, + { label: 'Reportes', path: '/reportes', requiredPermission: 'SS004' }, + { label: 'Radios', path: '/radios', requiredPermission: 'SS005' }, + { label: 'Usuarios', path: '/usuarios', requiredPermission: 'SS006' }, ]; const MainLayout: React.FC = ({ children }) => { const { - user, + user, // user ya está disponible aquí logout, isAuthenticated, isPasswordChangeForced, @@ -35,24 +40,40 @@ const MainLayout: React.FC = ({ children }) => { passwordChangeCompleted } = useAuth(); + const { tienePermiso, isSuperAdmin } = usePermissions(); // <<--- OBTENER HOOK DE PERMISOS const navigate = useNavigate(); const location = useLocation(); const [selectedTab, setSelectedTab] = useState(false); - const [anchorElUserMenu, setAnchorElUserMenu] = useState(null); // Estado para el menú de usuario + const [anchorElUserMenu, setAnchorElUserMenu] = useState(null); + + // --- INICIO DE CAMBIO: Filtrar módulos basados en permisos --- + const accessibleModules = useMemo(() => { + if (!isAuthenticated) return []; // Si no está autenticado, ningún módulo excepto quizás login (que no está aquí) + return allAppModules.filter(module => { + if (module.requiredPermission === null) return true; // Inicio siempre accesible + return isSuperAdmin || tienePermiso(module.requiredPermission); + }); + }, [isAuthenticated, isSuperAdmin, tienePermiso]); + // --- FIN DE CAMBIO --- useEffect(() => { - const currentModulePath = modules.findIndex(module => + // --- INICIO DE CAMBIO: Usar accessibleModules para encontrar el tab --- + const currentModulePath = accessibleModules.findIndex(module => location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/')) ); if (currentModulePath !== -1) { setSelectedTab(currentModulePath); } else if (location.pathname === '/') { - setSelectedTab(0); // Asegurar que la pestaña de Inicio se seleccione para la ruta raíz + // Asegurar que Inicio se seleccione si es accesible + const inicioIndex = accessibleModules.findIndex(m => m.path === '/'); + if (inicioIndex !== -1) setSelectedTab(inicioIndex); + else setSelectedTab(false); } else { - setSelectedTab(false); // Ninguna pestaña seleccionada si no coincide + setSelectedTab(false); } - }, [location.pathname]); + // --- FIN DE CAMBIO --- + }, [location.pathname, accessibleModules]); // << CAMBIO: dependencia a accessibleModules const handleOpenUserMenu = (event: React.MouseEvent) => { setAnchorElUserMenu(event.currentTarget); @@ -69,7 +90,7 @@ const MainLayout: React.FC = ({ children }) => { const handleLogoutClick = () => { logout(); - handleCloseUserMenu(); // Cierra el menú antes de desloguear completamente + handleCloseUserMenu(); }; const handleModalClose = (passwordChangedSuccessfully: boolean) => { @@ -77,23 +98,27 @@ const MainLayout: React.FC = ({ children }) => { passwordChangeCompleted(); } else { if (isPasswordChangeForced) { - logout(); + logout(); // Si es forzado y cancela/falla, desloguear } else { - setShowForcedPasswordChangeModal(false); + setShowForcedPasswordChangeModal(false); // Si no es forzado, solo cerrar modal } } }; const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { - setSelectedTab(newValue); - navigate(modules[newValue].path); + // --- INICIO DE CAMBIO: Navegar usando accessibleModules --- + if (accessibleModules[newValue]) { + setSelectedTab(newValue); + navigate(accessibleModules[newValue].path); + } + // --- FIN DE CAMBIO --- }; - // Determinar si el módulo actual es el de Reportes const isReportesModule = location.pathname.startsWith('/reportes'); if (showForcedPasswordChangeModal && isPasswordChangeForced) { - return ( + // ... (sin cambios) + return ( = ({ children }) => { ); } + + // Si no hay módulos accesibles después del login (y no es el cambio de clave forzado) + // Esto podría pasar si un usuario no tiene permiso para NINGUNA sección, ni siquiera Inicio. + // Deberías redirigir a login o mostrar un mensaje de "Sin acceso". + if (isAuthenticated && !isPasswordChangeForced && accessibleModules.length === 0) { + return ( + + No tiene acceso a ninguna sección del sistema. + + + ); + } + + return ( - + navigate('/')}> Sistema de Gestión - El Día - - {user && ( - + {/* ... (Menú de usuario sin cambios) ... */} + {user && ( + Hola, {user.nombreCompleto} )} @@ -125,9 +164,7 @@ const MainLayout: React.FC = ({ children }) => { aria-label="Cuenta del usuario" aria-controls="menu-appbar" aria-haspopup="true" - sx={{ - padding: '15px', - }} + sx={{ padding: '15px' }} onClick={handleOpenUserMenu} color="inherit" > @@ -143,15 +180,14 @@ const MainLayout: React.FC = ({ children }) => { onClose={handleCloseUserMenu} sx={{ '& .MuiPaper-root': { minWidth: 220, marginTop: '8px' } }} > - {user && ( // Mostrar info del usuario en el menú - + {user && ( + {user.nombreCompleto} {user.username} )} {user && } - - {!isPasswordChangeForced && ( // No mostrar si ya está forzado a cambiarla + {!isPasswordChangeForced && ( Cambiar Contraseña @@ -166,48 +202,45 @@ const MainLayout: React.FC = ({ children }) => { )} - - - {modules.map((module) => ( - - ))} - - + {/* --- INICIO DE CAMBIO: Renderizar Tabs solo si hay módulos accesibles y está autenticado --- */} + {isAuthenticated && accessibleModules.length > 0 && ( + + + {/* Mapear sobre accessibleModules en lugar de allAppModules */} + {accessibleModules.map((module) => ( + + ))} + + + )} + {/* --- FIN DE CAMBIO --- */} = ({ children }) => { {children} - `1px solid ${theme.palette.divider}` }}> + `1px solid ${theme.palette.divider}` + }}> - {/* Puedes usar caption para un texto más pequeño en el footer */} Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Administrador' : (user?.perfil || `ID ${user?.idPerfil}`)} handleModalClose(false)} // Asumir que si se cierra sin cambiar, no fue exitoso - isFirstLogin={false} // Este modal no es para el primer login forzado + open={showForcedPasswordChangeModal && !isPasswordChangeForced} + onClose={() => handleModalClose(false)} + isFirstLogin={false} /> ); diff --git a/Frontend/src/models/dtos/Contables/AjusteSaldoRequestDto.ts b/Frontend/src/models/dtos/Contables/AjusteSaldoRequestDto.ts new file mode 100644 index 0000000..e28a8a0 --- /dev/null +++ b/Frontend/src/models/dtos/Contables/AjusteSaldoRequestDto.ts @@ -0,0 +1,7 @@ +export interface AjusteSaldoRequestDto { + destino: 'Distribuidores' | 'Canillas'; + idDestino: number; + idEmpresa: number; + montoAjuste: number; + justificacion: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Contables/SaldoGestionDto.ts b/Frontend/src/models/dtos/Contables/SaldoGestionDto.ts new file mode 100644 index 0000000..bc7943d --- /dev/null +++ b/Frontend/src/models/dtos/Contables/SaldoGestionDto.ts @@ -0,0 +1,10 @@ +export interface SaldoGestionDto { + idSaldo: number; + destino: string; // "Distribuidores" o "Canillas" + idDestino: number; + nombreDestinatario: string; + idEmpresa: number; + nombreEmpresa: string; + monto: number; + fechaUltimaModificacion: string; // "yyyy-MM-ddTHH:mm:ss" o similar +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateNovedadCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/CreateNovedadCanillaDto.ts new file mode 100644 index 0000000..4f19887 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateNovedadCanillaDto.ts @@ -0,0 +1,5 @@ +export interface CreateNovedadCanillaDto { + idCanilla: number; + fecha: string; // string dd/MM/yyyy + detalle?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/DistribuidorDropdownDto.ts b/Frontend/src/models/dtos/Distribucion/DistribuidorDropdownDto.ts new file mode 100644 index 0000000..663cc44 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/DistribuidorDropdownDto.ts @@ -0,0 +1,4 @@ +export interface DistribuidorDropdownDto { + idDistribuidor: number; + nombre: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/DistribuidorLookupDto.ts b/Frontend/src/models/dtos/Distribucion/DistribuidorLookupDto.ts new file mode 100644 index 0000000..999e1a6 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/DistribuidorLookupDto.ts @@ -0,0 +1,4 @@ +export interface DistribuidorLookupDto { + idDistribuidor: number; + nombre: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/EmpresaDropdownDto.ts b/Frontend/src/models/dtos/Distribucion/EmpresaDropdownDto.ts new file mode 100644 index 0000000..79fd94f --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/EmpresaDropdownDto.ts @@ -0,0 +1,4 @@ +export interface EmpresaDropdownDto { + idEmpresa: number; + nombre: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/EmpresaLookupDto.ts b/Frontend/src/models/dtos/Distribucion/EmpresaLookupDto.ts new file mode 100644 index 0000000..e75f2f4 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/EmpresaLookupDto.ts @@ -0,0 +1,4 @@ +export interface EmpresaLookupDto { + idEmpresa: number; + nombre: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/NovedadCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/NovedadCanillaDto.ts new file mode 100644 index 0000000..b66c39f --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/NovedadCanillaDto.ts @@ -0,0 +1,7 @@ +export interface NovedadCanillaDto { + idNovedad: number; + idCanilla: number; + nombreCanilla: string; + fecha: string; // string dd/MM/yyyy + detalle?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateNovedadCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateNovedadCanillaDto.ts new file mode 100644 index 0000000..d73dbea --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateNovedadCanillaDto.ts @@ -0,0 +1,3 @@ +export interface UpdateNovedadCanillaDto { + detalle?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/CanillaGananciaReporteDto.ts b/Frontend/src/models/dtos/Reportes/CanillaGananciaReporteDto.ts new file mode 100644 index 0000000..4197fce --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/CanillaGananciaReporteDto.ts @@ -0,0 +1,8 @@ +export interface CanillaGananciaReporteDto { + canilla: string; // NomApe del canillita + legajo?: number | null; + francos?: number | null; + faltas?: number | null; + totalRendir?: number | null; + id?: string; // Para el DataGrid +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistCanMensualDiariosDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistCanMensualDiariosDto.ts new file mode 100644 index 0000000..4ecff9b --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistCanMensualDiariosDto.ts @@ -0,0 +1,10 @@ +export interface ListadoDistCanMensualDiariosDto { + canilla: string; + elDia: number | null; + elPlata: number | null; + vendidos: number | null; + importeElDia: number | null; + importeElPlata: number | null; + importeTotal: number | null; + id?: string; // Para DataGrid +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/ListadoDistCanMensualPubDto.ts b/Frontend/src/models/dtos/Reportes/ListadoDistCanMensualPubDto.ts new file mode 100644 index 0000000..ad2abb5 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/ListadoDistCanMensualPubDto.ts @@ -0,0 +1,8 @@ +export interface ListadoDistCanMensualPubDto { + publicacion: string; + canilla: string; + totalCantSalida: number | null; + totalCantEntrada: number | null; + totalRendir: number | null; + id?: string; // Para DataGrid +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Reportes/NovedadesCanillasReporteDto.ts b/Frontend/src/models/dtos/Reportes/NovedadesCanillasReporteDto.ts new file mode 100644 index 0000000..c83eb68 --- /dev/null +++ b/Frontend/src/models/dtos/Reportes/NovedadesCanillasReporteDto.ts @@ -0,0 +1,6 @@ +export interface NovedadesCanillasReporteDto { + nomApe: string; + fecha: string; + detalle?: string | null; + id?: string; // Para el DataGrid +} \ No newline at end of file diff --git a/Frontend/src/pages/Contables/ContablesIndexPage.tsx b/Frontend/src/pages/Contables/ContablesIndexPage.tsx index e76642b..fc008dc 100644 --- a/Frontend/src/pages/Contables/ContablesIndexPage.tsx +++ b/Frontend/src/pages/Contables/ContablesIndexPage.tsx @@ -7,6 +7,7 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom'; const contablesSubModules = [ { label: 'Pagos Distribuidores', path: 'pagos-distribuidores' }, { label: 'Notas Crédito/Débito', path: 'notas-cd' }, + { label: 'Gestión de Saldos', path: 'gestion-saldos' }, { label: 'Tipos de Pago', path: 'tipos-pago' }, ]; diff --git a/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx b/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx index 87ba974..32f2a08 100644 --- a/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx +++ b/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx @@ -56,7 +56,6 @@ const GestionarNotasCDPage: React.FC = () => { const [selectedRow, setSelectedRow] = useState(null); const { tienePermiso, isSuperAdmin } = usePermissions(); - // CN001 (Ver), CN002 (Crear), CN003 (Modificar), CN004 (Eliminar) const puedeVer = isSuperAdmin || tienePermiso("CN001"); const puedeCrear = isSuperAdmin || tienePermiso("CN002"); const puedeModificar = isSuperAdmin || tienePermiso("CN003"); diff --git a/Frontend/src/pages/Contables/GestionarSaldosPage.tsx b/Frontend/src/pages/Contables/GestionarSaldosPage.tsx new file mode 100644 index 0000000..96f342f --- /dev/null +++ b/Frontend/src/pages/Contables/GestionarSaldosPage.tsx @@ -0,0 +1,257 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, Paper, IconButton, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select +} from '@mui/material'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import EditNoteIcon from '@mui/icons-material/EditNote'; // Icono para ajustar saldo + +import saldoService from '../../services/Contables/saldoService'; +import empresaService from '../../services/Distribucion/empresaService'; +import distribuidorService from '../../services/Distribucion/distribuidorService'; +import canillaService from '../../services/Distribucion/canillaService'; + +import type { SaldoGestionDto } from '../../models/dtos/Contables/SaldoGestionDto'; +import type { AjusteSaldoRequestDto } from '../../models/dtos/Contables/AjusteSaldoRequestDto'; +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; +import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; +import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; + +import AjusteSaldoModal from '../../components/Modals/Contables/AjusteSaldoModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +type TipoDestinoFiltro = 'Distribuidores' | 'Canillas' | ''; + +const GestionarSaldosPage: React.FC = () => { + const [saldos, setSaldos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); // Para modal + + // Filtros + const [filtroTipoDestino, setFiltroTipoDestino] = useState(''); + const [filtroIdDestino, setFiltroIdDestino] = useState(''); + const [filtroIdEmpresa, setFiltroIdEmpresa] = useState(''); + + const [destinatariosDropdown, setDestinatariosDropdown] = useState<(DistribuidorDto | CanillaDto)[]>([]); + const [empresas, setEmpresas] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalAjusteOpen, setModalAjusteOpen] = useState(false); + const [saldoParaAjustar, setSaldoParaAjustar] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + // No necesitamos menú de acciones por fila si el ajuste es la única acción + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVerSaldos = isSuperAdmin || tienePermiso("CS001"); // Permiso para ver + const puedeAjustarSaldos = isSuperAdmin || tienePermiso("CS002"); // Permiso para ajustar + + + const fetchDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + setError(null); + try { + const empData = await empresaService.getAllEmpresas(); + setEmpresas(empData); + + if (filtroTipoDestino === 'Distribuidores') { + const distData = await distribuidorService.getAllDistribuidores(); + setDestinatariosDropdown(distData); + } else if (filtroTipoDestino === 'Canillas') { + const canData = await canillaService.getAllCanillas(undefined, undefined, true); // Solo activos + setDestinatariosDropdown(canData); + } else { + setDestinatariosDropdown([]); + } + } catch (err) { + console.error("Error cargando datos para filtros:", err); + setError("Error al cargar opciones de filtro."); + } finally { + setLoadingFiltersDropdown(false); + } + }, [filtroTipoDestino]); + + useEffect(() => { + fetchDropdownData(); + }, [fetchDropdownData]); + + + const cargarSaldos = useCallback(async () => { + if (!puedeVerSaldos) { + setError("No tiene permiso para ver saldos."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + destino: filtroTipoDestino || undefined, // Enviar undefined si está vacío + idDestino: filtroIdDestino ? Number(filtroIdDestino) : undefined, + idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined, + }; + const data = await saldoService.getAllSaldosGestion(params); + setSaldos(data); + } catch (err) { + console.error("Error al cargar saldos:", err); + setError('Error al cargar los saldos.'); + setSaldos([]); + } finally { + setLoading(false); + } + }, [puedeVerSaldos, filtroTipoDestino, filtroIdDestino, filtroIdEmpresa]); + + useEffect(() => { + cargarSaldos(); + }, [cargarSaldos]); + + + const handleOpenAjusteModal = (saldo: SaldoGestionDto) => { + if (!puedeAjustarSaldos) { + setApiErrorMessage("No tiene permiso para ajustar saldos."); + return; + } + setSaldoParaAjustar(saldo); + setApiErrorMessage(null); + setModalAjusteOpen(true); + }; + const handleCloseAjusteModal = () => { + setModalAjusteOpen(false); + setSaldoParaAjustar(null); + }; + + const handleSubmitAjusteModal = async (data: AjusteSaldoRequestDto) => { + if (!puedeAjustarSaldos) return; + setApiErrorMessage(null); + try { + await saldoService.ajustarSaldo(data); + cargarSaldos(); // Recargar lista para ver el saldo actualizado + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Error al aplicar el ajuste de saldo.'; + setApiErrorMessage(message); + throw err; // Para que el modal sepa que hubo error + } + }; + + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + + const displayData = saldos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleString('es-AR', {timeZone:'UTC'}) : '-'; + const formatCurrency = (value: number) => value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }); + + + if (!loading && !puedeVerSaldos) { + return {error || "Acceso denegado a esta sección."}; + } + + return ( + + Gestión de Saldos + + Filtros + + + Tipo Destinatario + + + + Destinatario Específico + + + + Empresa + + + + {/* No hay botón de "Agregar Saldo", se crean automáticamente */} + + + {loading && } + {error && !loading && !apiErrorMessage && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVerSaldos && ( + + + + Destinatario + Tipo + Empresa + Monto Saldo + Últ. Modificación + {puedeAjustarSaldos && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron saldos. + ) : ( + displayData.map((s) => ( + 0 ? 'rgba(0, 255, 0, 0.05)' : 'inherit')}} + > + {s.nombreDestinatario} + {s.destino} + {s.nombreEmpresa} + {formatCurrency(s.monto)} + {formatDate(s.fechaUltimaModificacion)} + {puedeAjustarSaldos && ( + + handleOpenAjusteModal(s)} color="primary"> + + + + )} + + )))} + +
+ +
+ )} + + {saldoParaAjustar && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarSaldosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx index 34bbff1..f9e7844 100644 --- a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx +++ b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx @@ -1,4 +1,3 @@ -// src/pages/configuracion/GestionarTiposPagoPage.tsx import React, { useState, useEffect, useCallback } from 'react'; import { Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, @@ -7,10 +6,10 @@ import { ListItemIcon, ListItemText } from '@mui/material'; -import AddIcon from '@mui/icons-material/Add'; // Icono para agregar -import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones -import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar -import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; import tipoPagoService from '../../services/Contables/tipoPagoService'; import type { TipoPago } from '../../models/Entities/TipoPago'; import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto'; @@ -30,20 +29,26 @@ const GestionarTiposPagoPage: React.FC = () => { const [apiErrorMessage, setApiErrorMessage] = useState(null); const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(5); + const [rowsPerPage, setRowsPerPage] = useState(25); // Cambiado a un valor más común - // Para el menú contextual de cada fila const [anchorEl, setAnchorEl] = useState(null); const [selectedTipoPagoRow, setSelectedTipoPagoRow] = useState(null); - const { tienePermiso, isSuperAdmin } = usePermissions(); // Obtener también isSuperAdmin + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVer = isSuperAdmin || tienePermiso("CT001"); // << AÑADIR ESTA LÍNEA const puedeCrear = isSuperAdmin || tienePermiso("CT002"); const puedeModificar = isSuperAdmin || tienePermiso("CT003"); const puedeEliminar = isSuperAdmin || tienePermiso("CT004"); const cargarTiposPago = useCallback(async () => { + if (!puedeVer) { // << AÑADIR CHEQUEO DE PERMISO AQUÍ + setError("No tiene permiso para ver los tipos de pago."); + setLoading(false); + setTiposPago([]); // Asegurar que no se muestren datos previos + return; + } setLoading(true); setError(null); try { @@ -55,7 +60,7 @@ const GestionarTiposPagoPage: React.FC = () => { } finally { setLoading(false); } - }, [filtroNombre]); + }, [filtroNombre, puedeVer]); // << AÑADIR puedeVer A LAS DEPENDENCIAS useEffect(() => { cargarTiposPago(); @@ -73,15 +78,15 @@ const GestionarTiposPagoPage: React.FC = () => { }; const handleSubmitModal = async (data: CreateTipoPagoDto | UpdateTipoPagoDto) => { - setApiErrorMessage(null); // Limpiar error previo + setApiErrorMessage(null); try { - if (editingTipoPago && 'idTipoPago' in data) { // Es Update + if (editingTipoPago && editingTipoPago.idTipoPago) { await tipoPagoService.updateTipoPago(editingTipoPago.idTipoPago, data as UpdateTipoPagoDto); - } else { // Es Create + } else { await tipoPagoService.createTipoPago(data as CreateTipoPagoDto); } - cargarTiposPago(); // Recargar lista - // onClose se llama desde el modal en caso de éxito + cargarTiposPago(); + // onClose se llama desde el modal si todo va bien } catch (err: any) { console.error("Error en submit modal (padre):", err); if (axios.isAxiosError(err) && err.response) { @@ -89,11 +94,12 @@ const GestionarTiposPagoPage: React.FC = () => { } else { setApiErrorMessage('Ocurrió un error inesperado al guardar.'); } - throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre + throw err; } }; const handleDelete = async (id: number) => { + // ... (sin cambios) if (window.confirm('¿Está seguro de que desea eliminar este tipo de pago?')) { setApiErrorMessage(null); try { @@ -126,12 +132,24 @@ const GestionarTiposPagoPage: React.FC = () => { }; const handleChangeRowsPerPage = (event: React.ChangeEvent) => { - setRowsPerPage(parseInt(event.target.value, 25)); + setRowsPerPage(parseInt(event.target.value, 10)); // << CORREGIDO: base 10, no 25 setPage(0); }; const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + // Renderizado condicional si no tiene permiso para ver + if (!loading && !puedeVer) { // << AÑADIR ESTE BLOQUE + return ( + + + Gestionar Tipos de Pago + + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + return ( @@ -146,10 +164,8 @@ const GestionarTiposPagoPage: React.FC = () => { size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} - // sx={{ flexGrow: 1 }} // Opcional, para que ocupe más espacio + disabled={!puedeVer || loading} // Deshabilitar si no puede ver o está cargando /> - {/* El botón de búsqueda se activa al cambiar el texto, pero puedes añadir uno explícito */} - {/* */} {puedeCrear && ( */} -
- {puedeCrear && ( + /> + } + label="Ver Activos" // Cambiado el label para más claridad + sx={{ flexShrink: 0 }} + /> +
+ {puedeCrear && ( - )} + )} {loading && } - {error && !loading && {error}} - {apiErrorMessage && {apiErrorMessage}} + {/* Mostrar error general si no hay error de API específico */} + {error && !apiErrorMessage && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} - {!loading && !error && puedeVer && ( - - - - LegajoNombre y Apellido - ZonaEmpresa - AccionistaEstado - Acciones - - - {displayData.length === 0 ? ( - No se encontraron canillitas. - ) : ( - displayData.map((c) => ( - - {c.legajo || '-'}{c.nomApe} - {c.nombreZona}{c.empresa === 0 ? '-' : c.nombreEmpresa} - {c.accionista ? : } - {c.baja ? : } - - handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}> - + {!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true también aquí + +
+ + LegajoNombre y Apellido + ZonaEmpresa + AccionistaEstado + {/* Mostrar acciones solo si tiene algún permiso para el menú */} + {(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron canillitas. + ) : ( + displayData.map((c) => ( + + {c.legajo || '-'}{c.nomApe} + {c.nombreZona}{c.empresa === 0 ? '-' : c.nombreEmpresa} + {c.accionista ? : } + {c.baja ? : } + {(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && ( + + handleMenuOpen(e, c)} + // Deshabilitar si NO tiene NINGUNO de los permisos para las acciones del menú + disabled={!puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla} + > + - - - )))} - -
- -
- )} + + )} + + )))} + + + + + )} - {puedeModificar && ( { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}> Modificar)} - {puedeDarBaja && selectedCanillitaRow && ( - handleToggleBaja(selectedCanillitaRow)}> - {selectedCanillitaRow.baja ? : } - {selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'} - + {/* Mostrar opción de Novedades si tiene permiso de ver canillitas o gestionar novedades */} + {puedeVerNovedadesCanilla && selectedCanillitaRow && ( + handleOpenNovedades(selectedCanillitaRow.idCanilla)}> + + Novedades + + )} + {puedeModificar && selectedCanillitaRow && ( // Asegurar que selectedCanillitaRow existe + { handleOpenModal(selectedCanillitaRow); handleMenuClose(); }}> + + Modificar + + )} + {puedeDarBaja && selectedCanillitaRow && ( + handleToggleBaja(selectedCanillitaRow)}> + {selectedCanillitaRow.baja ? : } + {selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'} + + )} + + {/* Mostrar "Sin acciones" si no hay ninguna acción permitida para la fila seleccionada */} + {selectedCanillitaRow && !puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla && ( + Sin acciones )} - {(!puedeModificar && !puedeDarBaja) && Sin acciones} { // Si no tiene permiso para ver, mostrar mensaje y salir if (!loading && !puedeVer) { return ( - - Gestionar Empresas + + Gestionar Empresas {error || "No tiene permiso para acceder a esta sección."} ); diff --git a/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx index c8bfcbd..a94cf38 100644 --- a/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx @@ -3,7 +3,8 @@ import { Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, CircularProgress, Alert, FormControl, InputLabel, Select, Checkbox, Tooltip, - Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle + Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, + ToggleButtonGroup, ToggleButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import PrintIcon from '@mui/icons-material/Print'; @@ -19,7 +20,7 @@ import canillaService from '../../services/Distribucion/canillaService'; import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; -import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto'; import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; @@ -28,25 +29,32 @@ import { usePermissions } from '../../hooks/usePermissions'; import axios from 'axios'; import reportesService from '../../services/Reportes/reportesService'; +type TipoDestinatarioFiltro = 'canillitas' | 'accionistas'; + const GestionarEntradasSalidasCanillaPage: React.FC = () => { const [movimientos, setMovimientos] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [apiErrorMessage, setApiErrorMessage] = useState(null); + const [loading, setLoading] = useState(true); // Para carga principal de movimientos + const [error, setError] = useState(null); // Error general o de carga + const [apiErrorMessage, setApiErrorMessage] = useState(null); // Para errores de modal/API - const [filtroFechaDesde, setFiltroFechaDesde] = useState(new Date().toISOString().split('T')[0]); - const [filtroFechaHasta, setFiltroFechaHasta] = useState(new Date().toISOString().split('T')[0]); + const [filtroFecha, setFiltroFecha] = useState(new Date().toISOString().split('T')[0]); const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); - const [filtroIdCanilla, setFiltroIdCanilla] = useState(''); - const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados'); - const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); + const [filtroIdCanillitaSeleccionado, setFiltroIdCanillitaSeleccionado] = useState(''); + const [filtroTipoDestinatario, setFiltroTipoDestinatario] = useState('canillitas'); - const [publicaciones, setPublicaciones] = useState([]); - const [canillitas, setCanillitas] = useState([]); + const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); + const [publicaciones, setPublicaciones] = useState([]); + const [destinatariosDropdown, setDestinatariosDropdown] = useState([]); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editingMovimiento, setEditingMovimiento] = useState(null); + const [prefillModalData, setPrefillModalData] = useState<{ + fecha?: string; + idCanilla?: number | string; + nombreCanilla?: string; // << AÑADIDO PARA PASAR AL MODAL + idPublicacion?: number | string; + } | null>(null); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(25); @@ -64,70 +72,123 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { const puedeLiquidar = isSuperAdmin || tienePermiso("MC005"); const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006"); - // Función para formatear fechas YYYY-MM-DD a DD/MM/YYYY const formatDate = (dateString?: string | null): string => { if (!dateString) return '-'; const datePart = dateString.split('T')[0]; const parts = datePart.split('-'); - if (parts.length === 3) { - return `${parts[2]}/${parts[1]}/${parts[0]}`; - } + if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; } return datePart; }; - const fetchFiltersDropdownData = useCallback(async () => { - setLoadingFiltersDropdown(true); - try { - const [pubsData, canData] = await Promise.all([ - publicacionService.getAllPublicaciones(undefined, undefined, true), - canillaService.getAllCanillas(undefined, undefined, true) - ]); - setPublicaciones(pubsData); - setCanillitas(canData); - } catch (err) { - console.error(err); setError("Error al cargar opciones de filtro."); - } finally { setLoadingFiltersDropdown(false); } + useEffect(() => { + const fetchPublicaciones = async () => { + setLoadingFiltersDropdown(true); // Mover al inicio de la carga de pubs + try { + const pubsData = await publicacionService.getPublicacionesForDropdown(true); + setPublicaciones(pubsData); + } catch (err) { + console.error("Error cargando publicaciones para filtro:",err); + setError("Error al cargar publicaciones."); // Usar error general + } finally { + // No poner setLoadingFiltersDropdown(false) aquí, esperar a que ambas cargas terminen + } + }; + fetchPublicaciones(); }, []); - useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); + const fetchDestinatariosParaDropdown = useCallback(async () => { + setLoadingFiltersDropdown(true); // Poner al inicio de esta carga también + setFiltroIdCanillitaSeleccionado(''); + setDestinatariosDropdown([]); + setError(null); // Limpiar errores de carga de dropdowns previos + try { + const esAccionistaFilter = filtroTipoDestinatario === 'accionistas'; + const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter); + setDestinatariosDropdown(data); + } catch (err) { + console.error("Error cargando destinatarios para filtro:", err); + setError("Error al cargar canillitas/accionistas."); // Usar error general + } finally { + setLoadingFiltersDropdown(false); // Poner al final de AMBAS cargas de dropdown + } + }, [filtroTipoDestinatario]); + + useEffect(() => { + fetchDestinatariosParaDropdown(); + }, [fetchDestinatariosParaDropdown]); + const cargarMovimientos = useCallback(async () => { - if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } + if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; } + if (!filtroFecha || !filtroIdCanillitaSeleccionado) { + if (loading) setLoading(false); + setMovimientos([]); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); try { - let liquidadosFilter: boolean | null = null; - let incluirNoLiquidadosFilter: boolean | null = true; // Por defecto mostrar no liquidados - - if (filtroEstadoLiquidacion === 'liquidados') { - liquidadosFilter = true; - incluirNoLiquidadosFilter = false; - } else if (filtroEstadoLiquidacion === 'noLiquidados') { - liquidadosFilter = false; - incluirNoLiquidadosFilter = true; - } // Si es 'todos', ambos son null o true y false respectivamente (backend debe manejarlo) - - const params = { - fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, + fechaDesde: filtroFecha, + fechaHasta: filtroFecha, idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, - idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null, - liquidados: liquidadosFilter, - incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter, + idCanilla: Number(filtroIdCanillitaSeleccionado), + liquidados: null, + incluirNoLiquidados: null, }; const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params); setMovimientos(data); - setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar + setSelectedIdsParaLiquidar(new Set()); } catch (err) { - console.error(err); setError('Error al cargar movimientos.'); - } finally { setLoading(false); } - }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]); + console.error("Error al cargar movimientos:", err); + setError('Error al cargar movimientos.'); + setMovimientos([]); + } finally { + setLoading(false); + } + }, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]); + + useEffect(() => { + if (filtroFecha && filtroIdCanillitaSeleccionado) { + cargarMovimientos(); + } else { + setMovimientos([]); + if (loading) setLoading(false); // Asegurar que no se quede en loading si los filtros se limpian + } + }, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); // `cargarMovimientos` ya tiene sus dependencias - useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]); const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { - setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); + if (!puedeCrear && !item) { + setApiErrorMessage("No tiene permiso para registrar nuevos movimientos."); + return; + } + if (item && !puedeModificar) { + setApiErrorMessage("No tiene permiso para modificar movimientos."); + return; + } + + if (item) { + setEditingMovimiento(item); + setPrefillModalData(null); + } else { + // --- CAMBIO: Obtener nombre del canillita seleccionado para prefill --- + const canillitaSeleccionado = destinatariosDropdown.find( + c => c.idCanilla === Number(filtroIdCanillitaSeleccionado) + ); + setEditingMovimiento(null); + setPrefillModalData({ + fecha: filtroFecha, + idCanilla: filtroIdCanillitaSeleccionado, + nombreCanilla: canillitaSeleccionado?.nomApe, // << AÑADIR NOMBRE + idPublicacion: filtroIdPublicacion + }); + } + setApiErrorMessage(null); + setModalOpen(true); }; + // ... handleDelete, handleMenuOpen, handleMenuClose, handleSelectRowForLiquidar, handleSelectAllForLiquidar, handleOpenLiquidarDialog, handleCloseLiquidarDialog sin cambios ... const handleDelete = async (idParte: number) => { if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) { setApiErrorMessage(null); @@ -138,7 +199,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { }; const handleMenuOpen = (event: React.MouseEvent, item: EntradaSalidaCanillaDto) => { - // Almacenar el idParte en el propio elemento del menú para referencia event.currentTarget.setAttribute('data-rowid', item.idParte.toString()); setAnchorEl(event.currentTarget); setSelectedRow(item); @@ -154,7 +214,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { }); }; const handleSelectAllForLiquidar = (event: React.ChangeEvent) => { - if (event.target.checked) { + if (event.target.checked) { const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte)); setSelectedIdsParaLiquidar(newSelectedIds); } else { @@ -170,74 +230,65 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { setOpenLiquidarDialog(true); }; const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false); + + const handleConfirmLiquidar = async () => { - if (selectedIdsParaLiquidar.size === 0) { - setApiErrorMessage("No hay movimientos seleccionados para liquidar."); - return; - } - if (!fechaLiquidacionDialog) { - setApiErrorMessage("Debe seleccionar una fecha de liquidación."); - return; - } - - // --- VALIDACIÓN DE FECHA --- - const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); // Usar Z para consistencia con formatDate si es necesario, o T00:00:00 para local - + if (selectedIdsParaLiquidar.size === 0) { /* ... */ return; } + if (!fechaLiquidacionDialog) { /* ... */ return; } + // ... (validación de fecha sin cambios) + const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); let fechaMovimientoMasReciente: Date | null = null; - selectedIdsParaLiquidar.forEach(idParte => { const movimiento = movimientos.find(m => m.idParte === idParte); - if (movimiento && movimiento.fecha) { // Asegurarse que movimiento.fecha existe - const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); // Consistencia con Z - if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime() + if (movimiento && movimiento.fecha) { + const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); + if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { fechaMovimientoMasReciente = movFecha; } } }); - - if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime() + if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`); return; } setApiErrorMessage(null); - setLoading(true); // Usar el loading general para la operación de liquidar - + setLoading(true); const liquidarDto: LiquidarMovimientosCanillaRequestDto = { idsPartesALiquidar: Array.from(selectedIdsParaLiquidar), - fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD + fechaLiquidacion: fechaLiquidacionDialog }; - try { await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto); - setOpenLiquidarDialog(false); - + setOpenLiquidarDialog(false); const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0]; const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado); - await cargarMovimientos(); + await cargarMovimientos(); - if (movimientoParaTicket) { - console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla); + // --- CAMBIO: NO IMPRIMIR TICKET SI ES ACCIONISTA --- + if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) { + console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla); await handleImprimirTicketLiquidacion( movimientoParaTicket.idCanilla, - fechaLiquidacionDialog, - movimientoParaTicket.canillaEsAccionista + fechaLiquidacionDialog, + false // esAccionista = false ); + } else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) { + console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente."); } else { - console.warn("No se pudo encontrar información del movimiento para generar el ticket post-liquidación."); + console.warn("No se pudo encontrar información del movimiento para ticket post-liquidación."); } - } catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.'; - setApiErrorMessage(msg); + setApiErrorMessage(msg); } finally { setLoading(false); } }; - // Esta función se pasa al modal para que la invoque al hacer submit en MODO EDICIÓN const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => { + // ... (sin cambios) setApiErrorMessage(null); try { await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data); @@ -251,32 +302,21 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); - // Recargar siempre que se cierre el modal y no haya un error pendiente a nivel de página - // Opcionalmente, podrías tener una bandera ' cambiosGuardados' que el modal active - // para ser más selectivo con la recarga. + setPrefillModalData(null); if (!apiErrorMessage) { cargarMovimientos(); } }; const handleImprimirTicketLiquidacion = useCallback(async ( - // Parámetros necesarios para el ticket - idCanilla: number, - fecha: string, // Fecha para la que se genera el ticket (probablemente fechaLiquidacionDialog) - esAccionista: boolean + idCanilla: number, fecha: string, esAccionista: boolean ) => { + // ... (sin cambios) setLoadingTicketPdf(true); setApiErrorMessage(null); - try { - const params = { - fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD - idCanilla: idCanilla, - esAccionista: esAccionista, - }; - + const params = { fecha: fecha.split('T')[0], idCanilla, esAccionista }; const blob = await reportesService.getTicketLiquidacionCanillaPdf(params); - if (blob.type === "application/json") { const text = await blob.text(); const msg = JSON.parse(text).message ?? "Error inesperado al generar el ticket PDF."; @@ -287,16 +327,11 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { if (!w) alert("Permita popups para ver el PDF del ticket."); } } catch (error: any) { - console.error("Error al generar ticket de liquidación:", error); - const message = axios.isAxiosError(error) && error.response?.data?.message - ? error.response.data.message - : 'Ocurrió un error al generar el ticket.'; + console.error("Error al generar ticket:", error); + const message = axios.isAxiosError(error) && error.response?.data?.message ? error.response.data.message : 'Error al generar ticket.'; setApiErrorMessage(message); - } finally { - setLoadingTicketPdf(false); - // No cerramos el menú aquí si se llama desde handleConfirmLiquidar - } - }, []); // Dependencias vacías si no usa nada del scope exterior que cambie, o añadir si es necesario + } finally { setLoadingTicketPdf(false); } + }, []); const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); @@ -305,47 +340,77 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { }; const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); - if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { // Modificado para solo mostrar si no hay filtros y no puede ver + return {error || "Acceso denegado."}; + } + const numSelectedToLiquidate = selectedIdsParaLiquidar.size; - // Corregido: numNotLiquidatedOnPage debe calcularse sobre 'movimientos' filtrados, no solo 'displayData' - // O, si la selección es solo por página, displayData está bien. Asumamos selección por página por ahora. const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length; return ( - Entradas/Salidas Canillitas + Entradas/Salidas Canillitas & Accionistas Filtros - setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> - setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + setFiltroFecha(e.target.value)} + InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} + required + error={!filtroFecha} // Se marca error si está vacío + helperText={!filtroFecha ? "Fecha es obligatoria" : ""} + /> + { + if (newValue !== null) { + setFiltroTipoDestinatario(newValue); + } + }} + aria-label="Tipo de Destinatario" + size="small" + > + Canillitas + Accionistas + + + + {filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'} + + {!filtroIdCanillitaSeleccionado && Selección obligatoria} + + - Publicación - setFiltroIdPublicacion(e.target.value as number | string)}> Todas {publicaciones.map(p => {p.nombre})} - - Canillita - - - - Estado Liquidación - - - {puedeCrear && ()} - {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && ( + {/* --- CAMBIO: DESHABILITAR BOTÓN SI FILTROS OBLIGATORIOS NO ESTÁN --- */} + {puedeCrear && ( + + )} + {puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && ( @@ -353,8 +418,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { + {!filtroFecha && Por favor, seleccione una fecha.} + {filtroFecha && !filtroIdCanillitaSeleccionado && Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.} + {loading && } - {error && !loading && !apiErrorMessage && {error}} + {/* Mostrar error general si no hay error de API específico y no está cargando filtros */} + {error && !loading && !apiErrorMessage && !loadingFiltersDropdown && {error}} {apiErrorMessage && {apiErrorMessage}} {loadingTicketPdf && @@ -364,12 +433,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { } - {!loading && !error && puedeVer && ( - + {!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && ( + // ... (Tabla y Paginación sin cambios) + - {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && ( + {puedeLiquidar && ( 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0} @@ -381,7 +451,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { )} Fecha Publicación - Canillita + {filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'} Salida Entrada Vendidos @@ -397,19 +467,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { - No se encontraron movimientos. + No se encontraron movimientos con los filtros aplicados. ) : ( displayData.map((m) => ( - {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && ( + {puedeLiquidar && ( { handleMenuOpen(e, m)} - data-rowid={m.idParte.toString()} // Guardar el id de la fila aquí - disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} // Lógica simplificada, refinar si es necesario + data-rowid={m.idParte.toString()} + disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} > @@ -462,19 +530,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { {puedeModificar && selectedRow && !selectedRow.liquidado && ( { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} - - {/* Opción de Imprimir Ticket Liq. */} - {selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir) + {/* --- CAMBIO: MOSTRAR REIMPRIMIR TICKET SIEMPRE SI ESTÁ LIQUIDADO --- */} + {selectedRow && selectedRow.liquidado && puedeLiquidar && ( // Usar puedeLiquidar para consistencia { - if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior + if (selectedRow) { handleImprimirTicketLiquidacion( selectedRow.idCanilla, - selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento - selectedRow.canillaEsAccionista + selectedRow.fechaLiquidado || selectedRow.fecha, + selectedRow.canillaEsAccionista // Pasar si es accionista ); } - // handleMenuClose() es llamado por handleImprimirTicketLiquidacion }} disabled={loadingTicketPdf} > @@ -483,13 +549,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { Reimprimir Ticket Liq. )} - - {selectedRow && ( // Opción de Eliminar + {selectedRow && ( ((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)) ) && ( - { - if (selectedRow) handleDelete(selectedRow.idParte); - }}> + { if (selectedRow) handleDelete(selectedRow.idParte); }}> Eliminar )} @@ -498,13 +561,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { setApiErrorMessage(null)} /> - + {/* ... (Dialog de Liquidación sin cambios) ... */} + Confirmar Liquidación @@ -523,7 +588,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { - ); }; diff --git a/Frontend/src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx b/Frontend/src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx new file mode 100644 index 0000000..7ded1e5 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx @@ -0,0 +1,301 @@ +// src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, Typography, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, + CircularProgress, Alert, TextField, Tooltip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import novedadCanillaService from '../../services/Distribucion/novedadCanillaService'; +import canillaService from '../../services/Distribucion/canillaService'; +import type { NovedadCanillaDto } from '../../models/dtos/Distribucion/NovedadCanillaDto'; +import type { CreateNovedadCanillaDto } from '../../models/dtos/Distribucion/CreateNovedadCanillaDto'; +import type { UpdateNovedadCanillaDto } from '../../models/dtos/Distribucion/UpdateNovedadCanillaDto'; +import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; +import NovedadCanillaFormModal from '../../components/Modals/Distribucion/NovedadCanillaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarNovedadesCanillaPage: React.FC = () => { + const { idCanilla: idCanillaStr } = useParams<{ idCanilla: string }>(); + const navigate = useNavigate(); + const idCanilla = Number(idCanillaStr); + + const [canillita, setCanillita] = useState(null); + const [novedades, setNovedades] = useState([]); + const [loading, setLoading] = useState(true); + const [errorPage, setErrorPage] = useState(null); // Error general de la página + + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingNovedad, setEditingNovedad] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); // Para modal/delete + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedNovedadRow, setSelectedNovedadRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006"); + const puedeVerCanillitas = isSuperAdmin || tienePermiso("CG001"); + + // Cargar datos del canillita (solo una vez o si idCanilla cambia) + useEffect(() => { + if (isNaN(idCanilla)) { + setErrorPage("ID de Canillita inválido."); + setLoading(false); // Detener carga principal + return; + } + if (!puedeVerCanillitas && !puedeGestionarNovedades) { + setErrorPage("No tiene permiso para acceder a esta sección."); + setLoading(false); + return; + } + + setLoading(true); // Iniciar carga para datos del canillita + const fetchCanillita = async () => { + try { + if (puedeVerCanillitas) { + const canData = await canillaService.getCanillaById(idCanilla); + setCanillita(canData); + } else { + // Si no puede ver detalles del canillita pero sí novedades, al menos mostrar ID + setCanillita({ idCanilla, nomApe: `ID ${idCanilla}` } as CanillaDto); + } + } catch (err) { + console.error("Error cargando datos del canillita:", err); + setErrorPage(`Error al cargar datos del canillita (ID: ${idCanilla}).`); + } + // No ponemos setLoading(false) aquí, porque la carga de novedades sigue. + }; + fetchCanillita(); + }, [idCanilla, puedeVerCanillitas, puedeGestionarNovedades]); + + + // Cargar/filtrar novedades + const cargarNovedades = useCallback(async () => { + if (isNaN(idCanilla) || (!puedeGestionarNovedades && !puedeVerCanillitas)) { + // Los permisos ya se validaron en el useEffect anterior, pero es bueno tenerlo + return; + } + // Si ya está cargando los datos del canillita, no iniciar otra carga paralela + // Se usará el mismo 'loading' para ambas operaciones iniciales. + // if (!loading) setLoading(true); // No es necesario si el useEffect anterior ya lo hizo + + setApiErrorMessage(null); // Limpiar errores de API de acciones previas + // setErrorPage(null); // No limpiar error de página aquí, podría ser por el canillita + + try { + const params = { + fechaDesde: filtroFechaDesde || null, + fechaHasta: filtroFechaHasta || null, + }; + const dataNovedades = await novedadCanillaService.getNovedadesPorCanilla(idCanilla, params); + setNovedades(dataNovedades); + // Si no hay datos con filtros, no es un error de API, simplemente no hay datos. + // El mensaje de "no hay novedades" se maneja en la tabla. + } catch (err: any) { + console.error("Error al cargar/filtrar novedades:", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Error al cargar las novedades.'; + setErrorPage(message); // Usar el error de página para problemas de carga de novedades + setNovedades([]); // Limpiar en caso de error + } finally { + // Solo poner setLoading(false) después de que AMBAS cargas (canillita y novedades) se intenten. + // Como se llaman en secuencia implícita por los useEffect, el último setLoading(false) es el de novedades. + setLoading(false); + } + }, [idCanilla, puedeGestionarNovedades, puedeVerCanillitas, filtroFechaDesde, filtroFechaHasta]); + + // useEffect para cargar novedades cuando los filtros o el canillita (o permisos) cambian + useEffect(() => { + // Solo cargar si tenemos un idCanilla válido y permisos + if (!isNaN(idCanilla) && (puedeGestionarNovedades || puedeVerCanillitas)) { + cargarNovedades(); + } else if (isNaN(idCanilla)){ + setErrorPage("ID de Canillita inválido."); + setLoading(false); + } else if (!puedeGestionarNovedades && !puedeVerCanillitas) { + setErrorPage("No tiene permiso para acceder a esta sección."); + setLoading(false); + } + }, [idCanilla, cargarNovedades, puedeGestionarNovedades, puedeVerCanillitas]); // `cargarNovedades` ya tiene sus dependencias + + + const handleOpenModal = (item?: NovedadCanillaDto) => { + if (!puedeGestionarNovedades) { + setApiErrorMessage("No tiene permiso para agregar o editar novedades."); + return; + } + setEditingNovedad(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingNovedad(null); + }; + + const handleSubmitModal = async (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => { + if (!puedeGestionarNovedades) return; + setApiErrorMessage(null); + try { + if (editingNovedad && idNovedad) { + await novedadCanillaService.updateNovedad(idNovedad, data as UpdateNovedadCanillaDto); + } else { + await novedadCanillaService.createNovedad(data as CreateNovedadCanillaDto); + } + cargarNovedades(); // Recargar lista de novedades + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la novedad.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idNovedadDelRow: number) => { + if (!puedeGestionarNovedades) return; + if (window.confirm(`¿Seguro de eliminar esta novedad (ID: ${idNovedadDelRow})?`)) { + setApiErrorMessage(null); + try { + await novedadCanillaService.deleteNovedad(idNovedadDelRow); + cargarNovedades(); // Recargar lista de novedades + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la novedad.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: NovedadCanillaDto) => { + setAnchorEl(event.currentTarget); setSelectedNovedadRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedNovedadRow(null); + }; + + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-'; + + + if (loading && !canillita) { // Muestra cargando solo si aún no tenemos los datos del canillita + return ; + } + + if (errorPage && !canillita) { // Si hay un error al cargar el canillita, no mostrar nada más + return {errorPage}; + } + + // Si no tiene permiso para la sección en general + if (!puedeGestionarNovedades && !puedeVerCanillitas) { + return Acceso denegado.; + } + + + return ( + + + + Novedades de: {canillita?.nomApe || `Canillita ID ${idCanilla}`} + + + + + {puedeGestionarNovedades && ( + + )} + + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} + disabled={loading} // Deshabilitar durante cualquier carga + /> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} + disabled={loading} // Deshabilitar durante cualquier carga + /> + + + + + {/* Mostrar error de API (de submit/delete) o error de carga de novedades */} + {(apiErrorMessage || (errorPage && novedades.length === 0 && !loading)) && ( + {apiErrorMessage || errorPage} + )} + + {loading && } + + +
+ + Fecha + Detalle de Novedad + {puedeGestionarNovedades && Acciones} + + + {novedades.length === 0 && !loading ? ( + + No hay novedades registradas { (filtroFechaDesde || filtroFechaHasta) && "con los filtros aplicados"}. + + ) : ( + novedades.map((nov) => ( + + {formatDate(nov.fecha)} + + + + {nov.detalle || '-'} + + + + {puedeGestionarNovedades && ( + + handleMenuOpen(e, nov)} disabled={!puedeGestionarNovedades}> + + + + )} + + )))} + +
+
+ + + {puedeGestionarNovedades && selectedNovedadRow && ( + { handleOpenModal(selectedNovedadRow); handleMenuClose(); }}> Editar)} + {puedeGestionarNovedades && selectedNovedadRow && ( + handleDelete(selectedNovedadRow.idNovedad)}> Eliminar)} + + + {idCanilla && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarNovedadesCanillaPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx index 1bffcc1..80844ff 100644 --- a/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx @@ -37,15 +37,16 @@ const GestionarOtrosDestinosPage: React.FC = () => { const { tienePermiso, isSuperAdmin } = usePermissions(); // Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso - const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad + const puedeVer = isSuperAdmin || tienePermiso("OD001"); const puedeCrear = isSuperAdmin || tienePermiso("OD002"); const puedeModificar = isSuperAdmin || tienePermiso("OD003"); const puedeEliminar = isSuperAdmin || tienePermiso("OD004"); const cargarOtrosDestinos = useCallback(async () => { if (!puedeVer) { - setError("No tiene permiso para ver esta sección."); + setError("No tiene permiso para ver los 'Otros Destinos'."); setLoading(false); + setOtrosDestinos([]); return; } setLoading(true); @@ -131,8 +132,8 @@ const GestionarOtrosDestinosPage: React.FC = () => { if (!loading && !puedeVer) { return ( - - Gestionar Otros Destinos + + Gestionar Otros Destinos {/* Cambiado h4 a h5 */} {error || "No tiene permiso para acceder a esta sección."} ); diff --git a/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx b/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx index 8ade640..1acc369 100644 --- a/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx @@ -241,7 +241,7 @@ const GestionarPublicacionesPage: React.FC = () => { setFiltroSoloHabilitadas(e.target.checked)} size="small" />} - label="Solo Habilitadas" + label="Ver Habilitadas" /> {puedeCrear && ()} diff --git a/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx b/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx index 62f4654..2c499da 100644 --- a/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx @@ -37,13 +37,19 @@ const GestionarZonasPage: React.FC = () => { const { tienePermiso, isSuperAdmin } = usePermissions(); - // Ajustar códigos de permiso para Zonas + const puedeVer = isSuperAdmin || tienePermiso("ZD001"); // Permiso para ver Zonas const puedeCrear = isSuperAdmin || tienePermiso("ZD002"); const puedeModificar = isSuperAdmin || tienePermiso("ZD003"); const puedeEliminar = isSuperAdmin || tienePermiso("ZD004"); const cargarZonas = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver las zonas."); + setLoading(false); + setZonas([]); // Asegurar que no se muestren datos previos + return; + } setLoading(true); setError(null); try { @@ -134,6 +140,17 @@ const GestionarZonasPage: React.FC = () => { // Adaptar para paginación const displayData = zonas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + if (!loading && !puedeVer) { + return ( + + + Gestionar Zonas + + {/* El error de "sin permiso" ya fue seteado en cargarZonas */} + {error || "No tiene permiso para acceder a esta sección."} + + ); + } return ( @@ -150,7 +167,6 @@ const GestionarZonasPage: React.FC = () => { value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} /> - {/* */} {puedeCrear && (