diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasDistController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasDistController.cs new file mode 100644 index 0000000..8186f5b --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasDistController.cs @@ -0,0 +1,138 @@ +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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; // Para BindRequired + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/entradassalidasdist")] // Ruta base + [ApiController] + [Authorize] + public class EntradasSalidasDistController : ControllerBase + { + private readonly IEntradaSalidaDistService _esService; + private readonly ILogger _logger; + + // Permisos para Entradas/Salidas Distribuidores (MD001, MD002) + // Asumo MD001 para Ver, MD002 para Crear/Modificar/Eliminar + private const string PermisoVerMovimientos = "MD001"; + private const string PermisoGestionarMovimientos = "MD002"; + + public EntradasSalidasDistController(IEntradaSalidaDistService esService, ILogger logger) + { + _esService = esService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + 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 EntradasSalidasDistController."); + return null; + } + + // GET: api/entradassalidasdist + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll( + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta, + [FromQuery] int? idPublicacion, [FromQuery] int? idDistribuidor, + [FromQuery] string? tipoMovimiento) + { + if (!TienePermiso(PermisoVerMovimientos)) return Forbid(); + try + { + var movimientos = await _esService.ObtenerTodosAsync(fechaDesde, fechaHasta, idPublicacion, idDistribuidor, tipoMovimiento); + return Ok(movimientos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener listado de Entradas/Salidas Dist."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // GET: api/entradassalidasdist/{idParte} + [HttpGet("{idParte:int}", Name = "GetEntradaSalidaDistById")] + [ProducesResponseType(typeof(EntradaSalidaDistDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int idParte) + { + if (!TienePermiso(PermisoVerMovimientos)) return Forbid(); + var movimiento = await _esService.ObtenerPorIdAsync(idParte); + if (movimiento == null) return NotFound(new { message = $"Movimiento con ID {idParte} no encontrado." }); + return Ok(movimiento); + } + + // POST: api/entradassalidasdist + [HttpPost] + [ProducesResponseType(typeof(EntradaSalidaDistDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateMovimiento([FromBody] CreateEntradaSalidaDistDto createDto) + { + if (!TienePermiso(PermisoGestionarMovimientos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _esService.CrearMovimientoAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar el movimiento."); + + return CreatedAtRoute("GetEntradaSalidaDistById", new { idParte = dto.IdParte }, dto); + } + + // PUT: api/entradassalidasdist/{idParte} + [HttpPut("{idParte:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateMovimiento(int idParte, [FromBody] UpdateEntradaSalidaDistDto updateDto) + { + if (!TienePermiso(PermisoGestionarMovimientos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _esService.ActualizarMovimientoAsync(idParte, updateDto, userId.Value); + if (!exito) + { + if (error == "Movimiento no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/entradassalidasdist/{idParte} + [HttpDelete("{idParte:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteMovimiento(int idParte) + { + if (!TienePermiso(PermisoGestionarMovimientos)) return Forbid(); // Asumo que el mismo permiso sirve para eliminar + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _esService.EliminarMovimientoAsync(idParte, userId.Value); + if (!exito) + { + if (error == "Movimiento no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/PorcentajesMonCanillaController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/PorcentajesMonCanillaController.cs new file mode 100644 index 0000000..b375686 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/PorcentajesMonCanillaController.cs @@ -0,0 +1,119 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/publicaciones/{idPublicacion}/porcentajesmoncanilla")] // Anidado + [ApiController] + [Authorize] + public class PorcentajesMonCanillaController : ControllerBase + { + private readonly IPorcMonCanillaService _porcMonCanillaService; + private readonly ILogger _logger; + + // Permiso CG004 para porcentajes de pago de canillitas + private const string PermisoGestionar = "CG004"; + + public PorcentajesMonCanillaController(IPorcMonCanillaService porcMonCanillaService, ILogger logger) + { + _porcMonCanillaService = porcMonCanillaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetPorcMonCanillaPorPublicacion(int idPublicacion) + { + // DP001 para ver publicación, CG004 para gestionar específicamente esto + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionar)) return Forbid(); + var items = await _porcMonCanillaService.ObtenerPorPublicacionIdAsync(idPublicacion); + return Ok(items); + } + + [HttpGet("{idPorcMon:int}", Name = "GetPorcMonCanillaById")] + [ProducesResponseType(typeof(PorcMonCanillaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPorcMonCanillaById(int idPublicacion, int idPorcMon) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionar)) return Forbid(); + var item = await _porcMonCanillaService.ObtenerPorIdAsync(idPorcMon); + if (item == null || item.IdPublicacion != idPublicacion) return NotFound(); + return Ok(item); + } + + [HttpPost] + [ProducesResponseType(typeof(PorcMonCanillaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreatePorcMonCanilla(int idPublicacion, [FromBody] CreatePorcMonCanillaDto createDto) + { + if (!TienePermiso(PermisoGestionar)) return Forbid(); + if (idPublicacion != createDto.IdPublicacion) + return BadRequest(new { message = "ID de publicación en ruta no coincide con el del cuerpo." }); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _porcMonCanillaService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear."); + return CreatedAtRoute("GetPorcMonCanillaById", new { idPublicacion = dto.IdPublicacion, idPorcMon = dto.IdPorcMon }, dto); + } + + [HttpPut("{idPorcMon:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePorcMonCanilla(int idPublicacion, int idPorcMon, [FromBody] UpdatePorcMonCanillaDto updateDto) + { + if (!TienePermiso(PermisoGestionar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var existente = await _porcMonCanillaService.ObtenerPorIdAsync(idPorcMon); + if (existente == null || existente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Registro no encontrado para esta publicación."}); + + var (exito, error) = await _porcMonCanillaService.ActualizarAsync(idPorcMon, updateDto, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + + [HttpDelete("{idPorcMon:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeletePorcMonCanilla(int idPublicacion, int idPorcMon) + { + if (!TienePermiso(PermisoGestionar)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var existente = await _porcMonCanillaService.ObtenerPorIdAsync(idPorcMon); + if (existente == null || existente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Registro no encontrado para esta publicación."}); + + var (exito, error) = await _porcMonCanillaService.EliminarAsync(idPorcMon, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/PorcentajesPagoController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/PorcentajesPagoController.cs new file mode 100644 index 0000000..404d586 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/PorcentajesPagoController.cs @@ -0,0 +1,118 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/publicaciones/{idPublicacion}/porcentajespago")] // Anidado + [ApiController] + [Authorize] + public class PorcentajesPagoController : ControllerBase // Para Porcentajes de Pago Distribuidores + { + private readonly IPorcPagoService _porcPagoService; + private readonly ILogger _logger; + + // Permiso DG004 para gestionar porcentajes de pago de distribuidores + private const string PermisoGestionarPorcentajes = "DG004"; + + public PorcentajesPagoController(IPorcPagoService porcPagoService, ILogger logger) + { + _porcPagoService = porcPagoService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetPorcentajesPorPublicacion(int idPublicacion) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarPorcentajes)) return Forbid(); // DP001 para ver Publicación + var porcentajes = await _porcPagoService.ObtenerPorPublicacionIdAsync(idPublicacion); + return Ok(porcentajes); + } + + [HttpGet("{idPorcentaje:int}", Name = "GetPorcPagoById")] + [ProducesResponseType(typeof(PorcPagoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPorcPagoById(int idPublicacion, int idPorcentaje) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarPorcentajes)) return Forbid(); + var porcPago = await _porcPagoService.ObtenerPorIdAsync(idPorcentaje); + if (porcPago == null || porcPago.IdPublicacion != idPublicacion) return NotFound(); + return Ok(porcPago); + } + + [HttpPost] + [ProducesResponseType(typeof(PorcPagoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreatePorcPago(int idPublicacion, [FromBody] CreatePorcPagoDto createDto) + { + if (!TienePermiso(PermisoGestionarPorcentajes)) return Forbid(); + if (idPublicacion != createDto.IdPublicacion) + return BadRequest(new { message = "ID de publicación en ruta no coincide con el del cuerpo." }); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _porcPagoService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear porcentaje."); + return CreatedAtRoute("GetPorcPagoById", new { idPublicacion = dto.IdPublicacion, idPorcentaje = dto.IdPorcentaje }, dto); + } + + [HttpPut("{idPorcentaje:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePorcPago(int idPublicacion, int idPorcentaje, [FromBody] UpdatePorcPagoDto updateDto) + { + if (!TienePermiso(PermisoGestionarPorcentajes)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var existente = await _porcPagoService.ObtenerPorIdAsync(idPorcentaje); + if (existente == null || existente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Porcentaje no encontrado para esta publicación."}); + + var (exito, error) = await _porcPagoService.ActualizarAsync(idPorcentaje, updateDto, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + + [HttpDelete("{idPorcentaje:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeletePorcPago(int idPublicacion, int idPorcentaje) + { + if (!TienePermiso(PermisoGestionarPorcentajes)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var existente = await _porcPagoService.ObtenerPorIdAsync(idPorcentaje); + if (existente == null || existente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Porcentaje no encontrado para esta publicación."}); + + var (exito, error) = await _porcPagoService.EliminarAsync(idPorcentaje, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/PubliSeccionesController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/PubliSeccionesController.cs new file mode 100644 index 0000000..48fd148 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/PubliSeccionesController.cs @@ -0,0 +1,124 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/publicaciones/{idPublicacion}/secciones")] // Anidado + [ApiController] + [Authorize] + public class PubliSeccionesController : ControllerBase + { + private readonly IPubliSeccionService _publiSeccionService; + private readonly ILogger _logger; + + // Permiso DP007 para gestionar secciones de publicaciones + private const string PermisoGestionarSecciones = "DP007"; + + public PubliSeccionesController(IPubliSeccionService publiSeccionService, ILogger logger) + { + _publiSeccionService = publiSeccionService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + // GET: api/publicaciones/{idPublicacion}/secciones + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetSeccionesPorPublicacion(int idPublicacion, [FromQuery] bool? soloActivas) + { + // DP001 para ver publicación, DP007 para gestionar específicamente secciones + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarSecciones)) return Forbid(); + var secciones = await _publiSeccionService.ObtenerPorPublicacionIdAsync(idPublicacion, soloActivas); + return Ok(secciones); + } + + // GET: api/publicaciones/{idPublicacion}/secciones/{idSeccion} + [HttpGet("{idSeccion:int}", Name = "GetPubliSeccionById")] + [ProducesResponseType(typeof(PubliSeccionDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPubliSeccionById(int idPublicacion, int idSeccion) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarSecciones)) return Forbid(); + var seccion = await _publiSeccionService.ObtenerPorIdAsync(idSeccion); + if (seccion == null || seccion.IdPublicacion != idPublicacion) return NotFound(); + return Ok(seccion); + } + + // POST: api/publicaciones/{idPublicacion}/secciones + [HttpPost] + [ProducesResponseType(typeof(PubliSeccionDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreatePubliSeccion(int idPublicacion, [FromBody] CreatePubliSeccionDto createDto) + { + if (!TienePermiso(PermisoGestionarSecciones)) return Forbid(); + if (idPublicacion != createDto.IdPublicacion) + return BadRequest(new { message = "ID de publicación en ruta no coincide con el del cuerpo." }); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _publiSeccionService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear sección."); + return CreatedAtRoute("GetPubliSeccionById", new { idPublicacion = dto.IdPublicacion, idSeccion = dto.IdSeccion }, dto); + } + + // PUT: api/publicaciones/{idPublicacion}/secciones/{idSeccion} + [HttpPut("{idSeccion:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePubliSeccion(int idPublicacion, int idSeccion, [FromBody] UpdatePubliSeccionDto updateDto) + { + if (!TienePermiso(PermisoGestionarSecciones)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var existente = await _publiSeccionService.ObtenerPorIdAsync(idSeccion); + if (existente == null || existente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Sección no encontrada para esta publicación."}); + + var (exito, error) = await _publiSeccionService.ActualizarAsync(idSeccion, updateDto, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + + // DELETE: api/publicaciones/{idPublicacion}/secciones/{idSeccion} + [HttpDelete("{idSeccion:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeletePubliSeccion(int idPublicacion, int idSeccion) + { + if (!TienePermiso(PermisoGestionarSecciones)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var existente = await _publiSeccionService.ObtenerPorIdAsync(idSeccion); + if (existente == null || existente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Sección no encontrada para esta publicación."}); + + var (exito, error) = await _publiSeccionService.EliminarAsync(idSeccion, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/RecargosZonaController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/RecargosZonaController.cs new file mode 100644 index 0000000..6647b60 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/RecargosZonaController.cs @@ -0,0 +1,123 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/publicaciones/{idPublicacion}/recargos")] // Anidado bajo publicaciones + [ApiController] + [Authorize] + public class RecargosZonaController : ControllerBase + { + private readonly IRecargoZonaService _recargoZonaService; + private readonly ILogger _logger; + + // Permiso DP005 para gestionar recargos + private const string PermisoGestionarRecargos = "DP005"; + + public RecargosZonaController(IRecargoZonaService recargoZonaService, ILogger logger) + { + _recargoZonaService = recargoZonaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + // GET: api/publicaciones/{idPublicacion}/recargos + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetRecargosPorPublicacion(int idPublicacion) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarRecargos)) return Forbid(); + var recargos = await _recargoZonaService.ObtenerPorPublicacionIdAsync(idPublicacion); + return Ok(recargos); + } + + // GET: api/publicaciones/{idPublicacion}/recargos/{idRecargo} + [HttpGet("{idRecargo:int}", Name = "GetRecargoZonaById")] + [ProducesResponseType(typeof(RecargoZonaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetRecargoZonaById(int idPublicacion, int idRecargo) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarRecargos)) return Forbid(); + var recargo = await _recargoZonaService.ObtenerPorIdAsync(idRecargo); + if (recargo == null || recargo.IdPublicacion != idPublicacion) return NotFound(); + return Ok(recargo); + } + + // POST: api/publicaciones/{idPublicacion}/recargos + [HttpPost] + [ProducesResponseType(typeof(RecargoZonaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateRecargoZona(int idPublicacion, [FromBody] CreateRecargoZonaDto createDto) + { + if (!TienePermiso(PermisoGestionarRecargos)) return Forbid(); + if (idPublicacion != createDto.IdPublicacion) + return BadRequest(new { message = "ID de publicación en ruta no coincide con el del cuerpo." }); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _recargoZonaService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear recargo."); + return CreatedAtRoute("GetRecargoZonaById", new { idPublicacion = dto.IdPublicacion, idRecargo = dto.IdRecargo }, dto); + } + + // PUT: api/publicaciones/{idPublicacion}/recargos/{idRecargo} + [HttpPut("{idRecargo:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateRecargoZona(int idPublicacion, int idRecargo, [FromBody] UpdateRecargoZonaDto updateDto) + { + if (!TienePermiso(PermisoGestionarRecargos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var recargoExistente = await _recargoZonaService.ObtenerPorIdAsync(idRecargo); + if (recargoExistente == null || recargoExistente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Recargo no encontrado para esta publicación."}); + + var (exito, error) = await _recargoZonaService.ActualizarAsync(idRecargo, updateDto, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + + // DELETE: api/publicaciones/{idPublicacion}/recargos/{idRecargo} + [HttpDelete("{idRecargo:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteRecargoZona(int idPublicacion, int idRecargo) + { + if (!TienePermiso(PermisoGestionarRecargos)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var recargoExistente = await _recargoZonaService.ObtenerPorIdAsync(idRecargo); + if (recargoExistente == null || recargoExistente.IdPublicacion != idPublicacion) + return NotFound(new { message = "Recargo no encontrado para esta publicación."}); + + var (exito, error) = await _recargoZonaService.EliminarAsync(idRecargo, userId.Value); + if (!exito) return BadRequest(new { message = error }); + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/SalidasOtrosDestinosController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/SalidasOtrosDestinosController.cs new file mode 100644 index 0000000..6f47d69 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/SalidasOtrosDestinosController.cs @@ -0,0 +1,128 @@ +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.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/[controller]")] // Ruta base: /api/salidasotrosdestinos + [ApiController] + [Authorize] + public class SalidasOtrosDestinosController : ControllerBase + { + private readonly ISalidaOtroDestinoService _salidaService; + private readonly ILogger _logger; + + // Permisos para Salidas Otros Destinos (SO001 a SO003, asumiendo OD00X para la entidad OtrosDestinos) + private const string PermisoVerSalidasOD = "SO001"; + private const string PermisoCrearSalidaOD = "SO002"; // Asumo SO002 para crear/modificar basado en tu excel + private const string PermisoModificarSalidaOD = "SO002"; // " + private const string PermisoEliminarSalidaOD = "SO003"; + + public SalidasOtrosDestinosController(ISalidaOtroDestinoService salidaService, ILogger logger) + { + _salidaService = salidaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + 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 SalidasOtrosDestinosController."); + return null; + } + + // GET: api/salidasotrosdestinos + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllSalidas( + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta, + [FromQuery] int? idPublicacion, [FromQuery] int? idDestino) + { + if (!TienePermiso(PermisoVerSalidasOD)) return Forbid(); + var salidas = await _salidaService.ObtenerTodosAsync(fechaDesde, fechaHasta, idPublicacion, idDestino); + return Ok(salidas); + } + + // GET: api/salidasotrosdestinos/{idParte} + [HttpGet("{idParte:int}", Name = "GetSalidaOtroDestinoById")] + [ProducesResponseType(typeof(SalidaOtroDestinoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetSalidaOtroDestinoById(int idParte) + { + if (!TienePermiso(PermisoVerSalidasOD)) return Forbid(); + var salida = await _salidaService.ObtenerPorIdAsync(idParte); + if (salida == null) return NotFound(); + return Ok(salida); + } + + // POST: api/salidasotrosdestinos + [HttpPost] + [ProducesResponseType(typeof(SalidaOtroDestinoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateSalidaOtroDestino([FromBody] CreateSalidaOtroDestinoDto createDto) + { + if (!TienePermiso(PermisoCrearSalidaOD)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _salidaService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar la salida."); + return CreatedAtRoute("GetSalidaOtroDestinoById", new { idParte = dto.IdParte }, dto); + } + + // PUT: api/salidasotrosdestinos/{idParte} + [HttpPut("{idParte:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateSalidaOtroDestino(int idParte, [FromBody] UpdateSalidaOtroDestinoDto updateDto) + { + if (!TienePermiso(PermisoModificarSalidaOD)) return Forbid(); // O SO002 si se usa para modificar + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _salidaService.ActualizarAsync(idParte, updateDto, userId.Value); + if (!exito) + { + if (error == "Registro de salida no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/salidasotrosdestinos/{idParte} + [HttpDelete("{idParte:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteSalidaOtroDestino(int idParte) + { + if (!TienePermiso(PermisoEliminarSalidaOD)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _salidaService.EliminarAsync(idParte, userId.Value); + if (!exito) + { + if (error == "Registro de salida no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs b/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs new file mode 100644 index 0000000..f93f806 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Impresion/StockBobinasController.cs @@ -0,0 +1,171 @@ +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Services.Impresion; +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.Impresion +{ + [Route("api/[controller]")] // Ruta base: /api/stockbobinas + [ApiController] + [Authorize] + public class StockBobinasController : ControllerBase + { + private readonly IStockBobinaService _stockBobinaService; + private readonly ILogger _logger; + + // Permisos para Stock de Bobinas (IB001 a IB005) + private const string PermisoVerStock = "IB001"; + private const string PermisoIngresarBobina = "IB002"; + private const string PermisoCambiarEstado = "IB003"; + private const string PermisoModificarDatos = "IB004"; // Para bobinas "Disponibles" + private const string PermisoEliminarBobina = "IB005"; // Para ingresos erróneos "Disponibles" + + + public StockBobinasController(IStockBobinaService stockBobinaService, ILogger logger) + { + _stockBobinaService = stockBobinaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + 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 StockBobinasController."); + return null; + } + + // GET: api/stockbobinas + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllStockBobinas( + [FromQuery] int? idTipoBobina, [FromQuery] string? nroBobina, [FromQuery] int? idPlanta, + [FromQuery] int? idEstadoBobina, [FromQuery] string? remito, + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta) + { + if (!TienePermiso(PermisoVerStock)) return Forbid(); + try + { + var bobinas = await _stockBobinaService.ObtenerTodosAsync(idTipoBobina, nroBobina, idPlanta, idEstadoBobina, remito, fechaDesde, fechaHasta); + return Ok(bobinas); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener el stock de bobinas."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // GET: api/stockbobinas/{idBobina} + [HttpGet("{idBobina:int}", Name = "GetStockBobinaById")] + [ProducesResponseType(typeof(StockBobinaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetStockBobinaById(int idBobina) + { + if (!TienePermiso(PermisoVerStock)) return Forbid(); + var bobina = await _stockBobinaService.ObtenerPorIdAsync(idBobina); + if (bobina == null) return NotFound(new { message = $"Bobina con ID {idBobina} no encontrada." }); + return Ok(bobina); + } + + // POST: api/stockbobinas (Ingresar nueva bobina) + [HttpPost] + [ProducesResponseType(typeof(StockBobinaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task IngresarBobina([FromBody] CreateStockBobinaDto createDto) + { + if (!TienePermiso(PermisoIngresarBobina)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _stockBobinaService.IngresarBobinaAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al ingresar la bobina."); + return CreatedAtRoute("GetStockBobinaById", new { idBobina = dto.IdBobina }, dto); + } + + // PUT: api/stockbobinas/{idBobina}/datos (Actualizar datos de una bobina DISPONIBLE) + [HttpPut("{idBobina:int}/datos")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateDatosBobinaDisponible(int idBobina, [FromBody] UpdateStockBobinaDto updateDto) + { + if (!TienePermiso(PermisoModificarDatos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _stockBobinaService.ActualizarDatosBobinaDisponibleAsync(idBobina, updateDto, userId.Value); + if (!exito) + { + if (error == "Bobina no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // PUT: api/stockbobinas/{idBobina}/cambiar-estado + [HttpPut("{idBobina:int}/cambiar-estado")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task CambiarEstadoBobina(int idBobina, [FromBody] CambiarEstadoBobinaDto cambiarEstadoDto) + { + if (!TienePermiso(PermisoCambiarEstado)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + // Validación adicional en el controlador para el caso "En Uso" + if (cambiarEstadoDto.NuevoEstadoId == 2) // Asumiendo 2 = En Uso + { + if (!cambiarEstadoDto.IdPublicacion.HasValue || cambiarEstadoDto.IdPublicacion.Value <= 0) + return BadRequest(new { message = "Se requiere IdPublicacion para el estado 'En Uso'."}); + if (!cambiarEstadoDto.IdSeccion.HasValue || cambiarEstadoDto.IdSeccion.Value <=0) + return BadRequest(new { message = "Se requiere IdSeccion para el estado 'En Uso'."}); + } + + + var (exito, error) = await _stockBobinaService.CambiarEstadoBobinaAsync(idBobina, cambiarEstadoDto, userId.Value); + if (!exito) + { + if (error == "Bobina no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/stockbobinas/{idBobina} (Eliminar un ingreso erróneo, solo si está "Disponible") + [HttpDelete("{idBobina:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si no está disponible o no se puede borrar + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteIngresoBobina(int idBobina) + { + if (!TienePermiso(PermisoEliminarBobina)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _stockBobinaService.EliminarIngresoErroneoAsync(idBobina, userId.Value); + if (!exito) + { + if (error == "Bobina no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Ej: "No está disponible" + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Impresion/TiradasController.cs b/Backend/GestionIntegral.Api/Controllers/Impresion/TiradasController.cs new file mode 100644 index 0000000..0b4866f --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Impresion/TiradasController.cs @@ -0,0 +1,112 @@ +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Services.Impresion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Impresion +{ + [Route("api/[controller]")] // Ruta base: /api/tiradas + [ApiController] + [Authorize] + public class TiradasController : ControllerBase + { + private readonly ITiradaService _tiradaService; + private readonly ILogger _logger; + + // Permisos para Tiradas (IT001 a IT003) + private const string PermisoVerTiradas = "IT001"; + private const string PermisoRegistrarTirada = "IT002"; + private const string PermisoEliminarTirada = "IT003"; // Asumo que se refiere a eliminar una tirada completa (cabecera y detalles) + + public TiradasController(ITiradaService tiradaService, ILogger logger) + { + _tiradaService = tiradaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + 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 TiradasController."); + return null; + } + + // GET: api/tiradas + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetTiradas( + [FromQuery] DateTime? fecha, + [FromQuery] int? idPublicacion, + [FromQuery] int? idPlanta) + { + if (!TienePermiso(PermisoVerTiradas)) return Forbid(); + try + { + var tiradas = await _tiradaService.ObtenerTiradasAsync(fecha, idPublicacion, idPlanta); + return Ok(tiradas); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener listado de tiradas."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // POST: api/tiradas + [HttpPost] + [ProducesResponseType(typeof(TiradaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task RegistrarTirada([FromBody] CreateTiradaRequestDto createDto) + { + if (!TienePermiso(PermisoRegistrarTirada)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (tiradaCreada, error) = await _tiradaService.RegistrarTiradaCompletaAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (tiradaCreada == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar la tirada."); + + // No hay un "GetById" simple para una tirada completa con un solo ID, + // ya que se identifica por Fecha, Publicacion, Planta. + // Podríamos devolver la tirada creada directamente. + return StatusCode(StatusCodes.Status201Created, tiradaCreada); + } + + // DELETE: api/tiradas + // Se identifica la tirada a eliminar por su combinación única de Fecha, IdPublicacion, IdPlanta + [HttpDelete] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] // Si no se encuentra la tirada para borrar + public async Task DeleteTirada( + [FromQuery, BindRequired] DateTime fecha, // Microsoft.AspNetCore.Mvc.ModelBinding.BindRequiredAttribute + [FromQuery, BindRequired] int idPublicacion, + [FromQuery, BindRequired] int idPlanta) + { + if (!TienePermiso(PermisoEliminarTirada)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _tiradaService.EliminarTiradaCompletaAsync(fecha, idPublicacion, idPlanta, userId.Value); + if (!exito) + { + if (error == "No se encontró una tirada principal para los criterios especificados.") + return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ 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 61a85d5..8486f78 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs @@ -1,7 +1,7 @@ using Dapper; -using GestionIntegral.Api.Data.Repositories; using GestionIntegral.Api.Models.Distribucion; using Microsoft.Extensions.Logging; +using System; // Para Exception using System.Collections.Generic; using System.Data; using System.Linq; @@ -24,10 +24,14 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos) { var sqlBuilder = new StringBuilder(@" - SELECT c.*, z.Nombre AS NombreZona, ISNULL(e.Nombre, 'N/A (Accionista)') AS NombreEmpresa + 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 -- Empresa 0 no tendrá join + LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa WHERE 1=1"); var parameters = new DynamicParameters(); @@ -37,13 +41,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } if (!string.IsNullOrWhiteSpace(nomApeFilter)) { - sqlBuilder.Append(" AND c.NomApe LIKE @NomApe"); - parameters.Add("NomApe", $"%{nomApeFilter}%"); + sqlBuilder.Append(" AND c.NomApe LIKE @NomApeParam"); + parameters.Add("NomApeParam", $"%{nomApeFilter}%"); } if (legajoFilter.HasValue) { - sqlBuilder.Append(" AND c.Legajo = @Legajo"); - parameters.Add("Legajo", legajoFilter.Value); + sqlBuilder.Append(" AND c.Legajo = @LegajoParam"); + parameters.Add("LegajoParam", legajoFilter.Value); } sqlBuilder.Append(" ORDER BY c.NomApe;"); @@ -59,7 +63,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } catch (Exception ex) { - _logger.LogError(ex, "Error al obtener todos los Canillas."); + _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)>(); } } @@ -67,18 +71,22 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id) { const string sql = @" - SELECT c.*, z.Nombre AS NombreZona, ISNULL(e.Nombre, 'N/A (Accionista)') AS NombreEmpresa + 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 c.Id_Canilla = @Id"; + WHERE c.Id_Canilla = @IdParam"; try { using var connection = _connectionFactory.CreateConnection(); - var result = await connection.QueryAsync( + var result = await connection.QueryAsync( sql, (canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa), - new { Id = id }, + new { IdParam = id }, splitOn: "NombreZona,NombreEmpresa" ); return result.SingleOrDefault(); @@ -89,25 +97,37 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion return (null, null, null); } } - public async Task GetByIdSimpleAsync(int id) // Para uso interno del servicio + public async Task GetByIdSimpleAsync(int id) { - const string sql = "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @Id"; - using var connection = _connectionFactory.CreateConnection(); - return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + const string sql = @" + SELECT + Id_Canilla AS IdCanilla, Legajo, NomApe, Parada, Id_Zona AS IdZona, + Accionista, Obs, Empresa, Baja, FechaBaja + FROM dbo.dist_dtCanillas + WHERE Id_Canilla = @IdParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParam = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Canilla (simple) por ID: {IdCanilla}", id); + return null; + } } - public async Task ExistsByLegajoAsync(int legajo, int? excludeIdCanilla = null) { - if (legajo == 0) return false; // Legajo 0 es como nulo, no debería validarse como único - var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtCanillas WHERE Legajo = @Legajo AND Legajo != 0"); // Excluir legajo 0 de la unicidad + if (legajo == 0) return false; + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtCanillas WHERE Legajo = @LegajoParam AND Legajo != 0"); var parameters = new DynamicParameters(); - parameters.Add("Legajo", legajo); + parameters.Add("LegajoParam", legajo); if (excludeIdCanilla.HasValue) { - sqlBuilder.Append(" AND Id_Canilla != @ExcludeIdCanilla"); - parameters.Add("ExcludeIdCanilla", excludeIdCanilla.Value); + sqlBuilder.Append(" AND Id_Canilla != @ExcludeIdCanillaParam"); + parameters.Add("ExcludeIdCanillaParam", excludeIdCanilla.Value); } try { @@ -125,23 +145,24 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { const string sqlInsert = @" INSERT INTO dbo.dist_dtCanillas (Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja) - OUTPUT INSERTED.* + OUTPUT INSERTED.Id_Canilla AS IdCanilla, INSERTED.Legajo, INSERTED.NomApe, INSERTED.Parada, INSERTED.Id_Zona AS IdZona, + INSERTED.Accionista, INSERTED.Obs, INSERTED.Empresa, INSERTED.Baja, INSERTED.FechaBaja VALUES (@Legajo, @NomApe, @Parada, @IdZona, @Accionista, @Obs, @Empresa, @Baja, @FechaBaja);"; - const string sqlInsertHistorico = @" - INSERT INTO dbo.dist_dtCanillas_H - (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdCanilla, @Legajo, @NomApe, @Parada, @IdZona, @Accionista, @Obs, @Empresa, @Baja, @FechaBaja, @Id_Usuario, @FechaMod, @TipoMod);"; - var connection = transaction.Connection!; var insertedCanilla = await connection.QuerySingleAsync(sqlInsert, nuevoCanilla, transaction); - if (insertedCanilla == null) throw new DataException("No se pudo crear el canilla."); + if (insertedCanilla == null || insertedCanilla.IdCanilla == 0) throw new DataException("No se pudo crear el canillita o obtener su ID."); + + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtCanillas_H + (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 { - insertedCanilla.IdCanilla, insertedCanilla.Legajo, insertedCanilla.NomApe, insertedCanilla.Parada, insertedCanilla.IdZona, - insertedCanilla.Accionista, insertedCanilla.Obs, insertedCanilla.Empresa, insertedCanilla.Baja, insertedCanilla.FechaBaja, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "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; } @@ -150,27 +171,29 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { var connection = transaction.Connection!; var canillaActual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanilla", new { canillaAActualizar.IdCanilla }, transaction); + @"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", + new { IdCanillaParam = canillaAActualizar.IdCanilla }, transaction); if (canillaActual == null) throw new KeyNotFoundException("Canilla no encontrado para actualizar."); const string sqlUpdate = @" UPDATE dbo.dist_dtCanillas SET Legajo = @Legajo, NomApe = @NomApe, Parada = @Parada, Id_Zona = @IdZona, Accionista = @Accionista, Obs = @Obs, Empresa = @Empresa - -- Baja y FechaBaja se manejan por ToggleBajaAsync WHERE Id_Canilla = @IdCanilla;"; const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtCanillas_H (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdCanilla, @LegajoActual, @NomApeActual, @ParadaActual, @IdZonaActual, @AccionistaActual, @ObsActual, @EmpresaActual, @BajaActual, @FechaBajaActual, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdCanillaParam, @LegajoParam, @NomApeParam, @ParadaParam, @IdZonaParam, @AccionistaParam, @ObsParam, @EmpresaParam, @BajaParam, @FechaBajaParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - IdCanilla = canillaActual.IdCanilla, - LegajoActual = canillaActual.Legajo, NomApeActual = canillaActual.NomApe, ParadaActual = canillaActual.Parada, IdZonaActual = canillaActual.IdZona, - AccionistaActual = canillaActual.Accionista, ObsActual = canillaActual.Obs, EmpresaActual = canillaActual.Empresa, - BajaActual = canillaActual.Baja, FechaBajaActual = canillaActual.FechaBaja, // Registrar estado actual de baja - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizado" + 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" }, transaction); var rowsAffected = await connection.ExecuteAsync(sqlUpdate, canillaAActualizar, transaction); @@ -181,24 +204,27 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { var connection = transaction.Connection!; var canillaActual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanilla", new { IdCanilla = id }, transaction); + @"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", + new { IdCanillaParam = id }, transaction); if (canillaActual == null) throw new KeyNotFoundException("Canilla no encontrado para dar de baja/alta."); - const string sqlUpdate = "UPDATE dbo.dist_dtCanillas SET Baja = @Baja, FechaBaja = @FechaBaja WHERE Id_Canilla = @IdCanilla;"; + const string sqlUpdate = "UPDATE dbo.dist_dtCanillas SET Baja = @BajaParam, FechaBaja = @FechaBajaParam WHERE Id_Canilla = @IdCanillaParam;"; const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtCanillas_H (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdCanilla, @Legajo, @NomApe, @Parada, @IdZona, @Accionista, @Obs, @Empresa, @BajaNueva, @FechaBajaNueva, @Id_Usuario, @FechaMod, @TipoModHist);"; + VALUES (@IdCanillaParam, @LegajoParam, @NomApeParam, @ParadaParam, @IdZonaParam, @AccionistaParam, @ObsParam, @EmpresaParam, @BajaNuevaParam, @FechaBajaNuevaParam, @Id_UsuarioParam, @FechaModParam, @TipoModHistParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - canillaActual.IdCanilla, canillaActual.Legajo, canillaActual.NomApe, canillaActual.Parada, canillaActual.IdZona, - canillaActual.Accionista, canillaActual.Obs, canillaActual.Empresa, - BajaNueva = darDeBaja, FechaBajaNueva = (darDeBaja ? fechaBaja : null), // FechaBaja solo si se da de baja - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoModHist = (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 { Baja = darDeBaja, FechaBaja = (darDeBaja ? fechaBaja : null), IdCanilla = id }, transaction); + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { BajaParam = darDeBaja, FechaBajaParam = (darDeBaja ? fechaBaja : null), IdCanillaParam = id }, transaction); return rowsAffected == 1; } } diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs index d029907..73e0dd6 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs @@ -1,6 +1,7 @@ using Dapper; using GestionIntegral.Api.Models.Distribucion; using Microsoft.Extensions.Logging; +using System; // Añadido para Exception using System.Collections.Generic; using System.Data; using System.Linq; @@ -23,7 +24,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task> GetAllAsync(string? nombreFilter, string? nroDocFilter) { var sqlBuilder = new StringBuilder(@" - SELECT d.*, z.Nombre AS NombreZona + SELECT + d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona, + d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad, + z.Nombre AS NombreZona FROM dbo.dist_dtDistribuidores d LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona WHERE 1=1"); @@ -31,13 +35,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion if (!string.IsNullOrWhiteSpace(nombreFilter)) { - sqlBuilder.Append(" AND d.Nombre LIKE @Nombre"); - parameters.Add("Nombre", $"%{nombreFilter}%"); + sqlBuilder.Append(" AND d.Nombre LIKE @NombreParam"); + parameters.Add("NombreParam", $"%{nombreFilter}%"); } if (!string.IsNullOrWhiteSpace(nroDocFilter)) { - sqlBuilder.Append(" AND d.NroDoc LIKE @NroDoc"); - parameters.Add("NroDoc", $"%{nroDocFilter}%"); + sqlBuilder.Append(" AND d.NroDoc LIKE @NroDocParam"); + parameters.Add("NroDocParam", $"%{nroDocFilter}%"); } sqlBuilder.Append(" ORDER BY d.Nombre;"); @@ -53,7 +57,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion } catch (Exception ex) { - _logger.LogError(ex, "Error al obtener todos los Distribuidores."); + _logger.LogError(ex, "Error al obtener todos los Distribuidores. Filtros: Nombre='{NombreFilter}', NroDoc='{NroDocFilter}'", nombreFilter, nroDocFilter); return Enumerable.Empty<(Distribuidor, string?)>(); } } @@ -61,17 +65,20 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id) { const string sql = @" - SELECT d.*, z.Nombre AS NombreZona + SELECT + d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona, + d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad, + z.Nombre AS NombreZona FROM dbo.dist_dtDistribuidores d LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona - WHERE d.Id_Distribuidor = @Id"; + WHERE d.Id_Distribuidor = @IdParam"; try { using var connection = _connectionFactory.CreateConnection(); - var result = await connection.QueryAsync( + var result = await connection.QueryAsync( sql, (dist, zona) => (dist, zona), - new { Id = id }, + new { IdParam = id }, splitOn: "NombreZona" ); return result.SingleOrDefault(); @@ -85,73 +92,112 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task GetByIdSimpleAsync(int id) { - const string sql = "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id"; - using var connection = _connectionFactory.CreateConnection(); - return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + const string sql = @" + SELECT + Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona, + Calle, Numero, Piso, Depto, Telefono, Email, Localidad + 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 (simple) por ID: {IdDistribuidor}", id); + return null; + } } public async Task ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null) { - var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE NroDoc = @NroDoc"); + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE NroDoc = @NroDocParam"); var parameters = new DynamicParameters(); - parameters.Add("NroDoc", nroDoc); + parameters.Add("NroDocParam", nroDoc); if (excludeIdDistribuidor.HasValue) { - sqlBuilder.Append(" AND Id_Distribuidor != @ExcludeId"); - parameters.Add("ExcludeId", excludeIdDistribuidor.Value); + sqlBuilder.Append(" AND Id_Distribuidor != @ExcludeIdParam"); + parameters.Add("ExcludeIdParam", excludeIdDistribuidor.Value); + } + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNroDocAsync. NroDoc: {NroDoc}", nroDoc); + return true; } - using var connection = _connectionFactory.CreateConnection(); - return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); } public async Task ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null) { - var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE Nombre = @Nombre"); + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE Nombre = @NombreParam"); var parameters = new DynamicParameters(); - parameters.Add("Nombre", nombre); + parameters.Add("NombreParam", nombre); if (excludeIdDistribuidor.HasValue) { - sqlBuilder.Append(" AND Id_Distribuidor != @ExcludeId"); - parameters.Add("ExcludeId", excludeIdDistribuidor.Value); + sqlBuilder.Append(" AND Id_Distribuidor != @ExcludeIdParam"); + parameters.Add("ExcludeIdParam", excludeIdDistribuidor.Value); + } + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync. Nombre: {Nombre}", nombre); + return true; } - using var connection = _connectionFactory.CreateConnection(); - return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); } public async Task IsInUseAsync(int id) { using var connection = _connectionFactory.CreateConnection(); string[] checkQueries = { - "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidas WHERE Id_Distribuidor = @Id", - "SELECT TOP 1 1 FROM dbo.cue_PagosDistribuidor WHERE Id_Distribuidor = @Id", - "SELECT TOP 1 1 FROM dbo.dist_PorcPago WHERE Id_Distribuidor = @Id" + "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidas WHERE Id_Distribuidor = @IdParam", + "SELECT TOP 1 1 FROM dbo.cue_PagosDistribuidor WHERE Id_Distribuidor = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_PorcPago WHERE Id_Distribuidor = @IdParam" }; - foreach (var query in checkQueries) + try { - if (await connection.ExecuteScalarAsync(query, new { Id = id }) == 1) return true; + foreach (var query in checkQueries) + { + if (await connection.ExecuteScalarAsync(query, new { IdParam = id }) == 1) return true; + } + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Distribuidor ID: {IdDistribuidor}", id); + return true; } - return false; } public async Task CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction) { const string sqlInsert = @" INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad) - OUTPUT INSERTED.* + OUTPUT INSERTED.Id_Distribuidor AS IdDistribuidor, INSERTED.Nombre, INSERTED.Contacto, INSERTED.NroDoc, INSERTED.Id_Zona AS IdZona, + INSERTED.Calle, INSERTED.Numero, INSERTED.Piso, INSERTED.Depto, INSERTED.Telefono, INSERTED.Email, INSERTED.Localidad VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad);"; + + var connection = transaction.Connection!; + var inserted = await connection.QuerySingleAsync(sqlInsert, nuevoDistribuidor, transaction); + if (inserted == null || inserted.IdDistribuidor == 0) throw new DataException("Error al crear distribuidor o al obtener el ID generado."); + const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtDistribuidores_H (Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdDistribuidor, @Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, @Id_Usuario, @FechaMod, @TipoMod);"; - - var connection = transaction.Connection!; - var inserted = await connection.QuerySingleAsync(sqlInsert, nuevoDistribuidor, transaction); - if (inserted == null) throw new DataException("Error al crear distribuidor."); + VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - inserted.IdDistribuidor, inserted.Nombre, inserted.Contacto, inserted.NroDoc, inserted.IdZona, - inserted.Calle, inserted.Numero, inserted.Piso, inserted.Depto, inserted.Telefono, inserted.Email, inserted.Localidad, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creado" + IdDistribuidorParam = inserted.IdDistribuidor, NombreParam = inserted.Nombre, ContactoParam = inserted.Contacto, NroDocParam = inserted.NroDoc, IdZonaParam = inserted.IdZona, + CalleParam = inserted.Calle, NumeroParam = inserted.Numero, PisoParam = inserted.Piso, DeptoParam = inserted.Depto, TelefonoParam = inserted.Telefono, EmailParam = inserted.Email, LocalidadParam = inserted.Localidad, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Creado" }, transaction); return inserted; } @@ -160,8 +206,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { var connection = transaction.Connection!; var actual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidor", - new { distribuidorAActualizar.IdDistribuidor }, transaction); + @"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona, + Calle, Numero, Piso, Depto, Telefono, Email, Localidad + FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam", + new { IdDistribuidorParam = distribuidorAActualizar.IdDistribuidor }, transaction); if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado."); const string sqlUpdate = @" @@ -172,13 +220,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtDistribuidores_H (Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdDistribuidor, @Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - IdDistribuidor = actual.IdDistribuidor, Nombre = actual.Nombre, Contacto = actual.Contacto, NroDoc = actual.NroDoc, IdZona = actual.IdZona, - Calle = actual.Calle, Numero = actual.Numero, Piso = actual.Piso, Depto = actual.Depto, Telefono = actual.Telefono, Email = actual.Email, Localidad = actual.Localidad, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizado" + IdDistribuidorParam = actual.IdDistribuidor, NombreParam = actual.Nombre, ContactoParam = actual.Contacto, NroDocParam = actual.NroDoc, IdZonaParam = actual.IdZona, + CalleParam = actual.Calle, NumeroParam = actual.Numero, PisoParam = actual.Piso, DeptoParam = actual.Depto, TelefonoParam = actual.Telefono, EmailParam = actual.Email, LocalidadParam = actual.Localidad, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Actualizado" }, transaction); var rowsAffected = await connection.ExecuteAsync(sqlUpdate, distribuidorAActualizar, transaction); @@ -189,23 +237,26 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { var connection = transaction.Connection!; var actual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id", new { Id = id }, transaction); + @"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona, + Calle, Numero, Piso, Depto, Telefono, Email, Localidad + FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam", new { IdParam = id }, transaction); if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado."); - const string sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id"; + // ... (resto del método DeleteAsync) + const string sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam"; const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtDistribuidores_H (Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdDistribuidor, @Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - IdDistribuidor = actual.IdDistribuidor, actual.Nombre, actual.Contacto, actual.NroDoc, actual.IdZona, - actual.Calle, actual.Numero, actual.Piso, actual.Depto, actual.Telefono, actual.Email, actual.Localidad, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Eliminado" + IdDistribuidorParam = actual.IdDistribuidor, NombreParam = actual.Nombre, ContactoParam = actual.Contacto, NroDocParam = actual.NroDoc, IdZonaParam = actual.IdZona, + CalleParam = actual.Calle, NumeroParam = actual.Numero, PisoParam = actual.Piso, DeptoParam = actual.Depto, TelefonoParam = actual.Telefono, EmailParam = actual.Email, LocalidadParam = actual.Localidad, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Eliminado" }, transaction); - var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction); + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { IdParam = id }, transaction); return rowsAffected == 1; } } diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaDistRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaDistRepository.cs new file mode 100644 index 0000000..8512d8f --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaDistRepository.cs @@ -0,0 +1,180 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class EntradaSalidaDistRepository : IEntradaSalidaDistRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public EntradaSalidaDistRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + public async Task> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento) + { + var sqlBuilder = new StringBuilder(@" + SELECT + Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + Fecha, TipoMovimiento, Cantidad, Remito, Observacion, + Id_Precio AS IdPrecio, Id_Recargo AS IdRecargo, Id_Porcentaje AS IdPorcentaje + FROM dbo.dist_EntradasSalidas + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) { sqlBuilder.Append(" AND Fecha >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } + if (fechaHasta.HasValue) { sqlBuilder.Append(" AND Fecha <= @FechaHastaParam"); parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } + if (idPublicacion.HasValue) { sqlBuilder.Append(" AND Id_Publicacion = @IdPublicacionParam"); parameters.Add("IdPublicacionParam", idPublicacion.Value); } + if (idDistribuidor.HasValue) { sqlBuilder.Append(" AND Id_Distribuidor = @IdDistribuidorParam"); parameters.Add("IdDistribuidorParam", idDistribuidor.Value); } + if (!string.IsNullOrWhiteSpace(tipoMovimiento)) { sqlBuilder.Append(" AND TipoMovimiento = @TipoMovimientoParam"); parameters.Add("TipoMovimientoParam", tipoMovimiento); } + sqlBuilder.Append(" ORDER BY Fecha DESC, Id_Parte DESC;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Entradas/Salidas de Distribuidores."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idParte) + { + const string sql = @" + SELECT + Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + Fecha, TipoMovimiento, Cantidad, Remito, Observacion, + Id_Precio AS IdPrecio, Id_Recargo AS IdRecargo, Id_Porcentaje AS IdPorcentaje + FROM dbo.dist_EntradasSalidas + WHERE Id_Parte = @IdParteParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParteParam = idParte }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener EntradaSalidaDist por ID: {IdParte}", idParte); + return null; + } + } + + public async Task ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_EntradasSalidas WHERE Remito = @RemitoParam AND TipoMovimiento = @TipoMovimientoParam AND Id_Publicacion = @IdPublicacionParam"); + var parameters = new DynamicParameters(); + parameters.Add("RemitoParam", remito); + parameters.Add("TipoMovimientoParam", tipoMovimiento); + parameters.Add("IdPublicacionParam", idPublicacion); + + if (excludeIdParte.HasValue) + { + sqlBuilder.Append(" AND Id_Parte != @ExcludeIdParteParam"); + parameters.Add("ExcludeIdParteParam", excludeIdParte.Value); + } + try + { + using var connection = _cf.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error en ExistsByRemitoAndTipoForPublicacionAsync. Remito: {Remito}", remito); + return true; // Asumir que existe en caso de error para prevenir duplicados + } + } + + + public async Task CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_EntradasSalidas (Id_Publicacion, Id_Distribuidor, Fecha, TipoMovimiento, Cantidad, Remito, Observacion, Id_Precio, Id_Recargo, Id_Porcentaje) + OUTPUT INSERTED.Id_Parte AS IdParte, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Distribuidor AS IdDistribuidor, + INSERTED.Fecha, INSERTED.TipoMovimiento, INSERTED.Cantidad, INSERTED.Remito, INSERTED.Observacion, + INSERTED.Id_Precio AS IdPrecio, INSERTED.Id_Recargo AS IdRecargo, INSERTED.Id_Porcentaje AS IdPorcentaje + VALUES (@IdPublicacion, @IdDistribuidor, @Fecha, @TipoMovimiento, @Cantidad, @Remito, @Observacion, @IdPrecio, @IdRecargo, @IdPorcentaje);"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidas_H + (Id_Parte, Id_Publicacion, Id_Distribuidor, Fecha, TipoMovimiento, Cantidad, Remito, Observacion, Id_Precio, Id_Recargo, Id_Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPublicacionHist, @IdDistribuidorHist, @FechaHist, @TipoMovimientoHist, @CantidadHist, @RemitoHist, @ObservacionHist, @IdPrecioHist, @IdRecargoHist, @IdPorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaES, transaction); + if (inserted == null || inserted.IdParte == 0) throw new DataException("Error al crear Entrada/Salida o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdParteHist = inserted.IdParte, IdPublicacionHist = inserted.IdPublicacion, IdDistribuidorHist = inserted.IdDistribuidor, + FechaHist = inserted.Fecha, TipoMovimientoHist = inserted.TipoMovimiento, CantidadHist = inserted.Cantidad, RemitoHist = inserted.Remito, ObservacionHist = inserted.Observacion, + IdPrecioHist = inserted.IdPrecio, IdRecargoHist = inserted.IdRecargo, IdPorcentajeHist = inserted.IdPorcentaje, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Creada" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, Fecha, TipoMovimiento, Cantidad, Remito, Observacion, Id_Precio AS IdPrecio, Id_Recargo AS IdRecargo, Id_Porcentaje AS IdPorcentaje + FROM dbo.dist_EntradasSalidas WHERE Id_Parte = @IdParteParam", + new { IdParteParam = esAActualizar.IdParte }, transaction); + if (actual == null) throw new KeyNotFoundException("Registro de Entrada/Salida no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_EntradasSalidas SET + Cantidad = @Cantidad, Observacion = @Observacion + -- Publicacion, Distribuidor, Fecha, TipoMovimiento, Remito, Ids de precio/recargo/porc no se modifican aquí + WHERE Id_Parte = @IdParte;"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidas_H + (Id_Parte, Id_Publicacion, Id_Distribuidor, Fecha, TipoMovimiento, Cantidad, Remito, Observacion, Id_Precio, Id_Recargo, Id_Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPublicacionHist, @IdDistribuidorHist, @FechaHist, @TipoMovimientoHist, @CantidadHist, @RemitoHist, @ObservacionHist, @IdPrecioHist, @IdRecargoHist, @IdPorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdParteHist = actual.IdParte, IdPublicacionHist = actual.IdPublicacion, IdDistribuidorHist = actual.IdDistribuidor, + FechaHist = actual.Fecha, TipoMovimientoHist = actual.TipoMovimiento, CantidadHist = actual.Cantidad, RemitoHist = actual.Remito, ObservacionHist = actual.Observacion, + IdPrecioHist = actual.IdPrecio, IdRecargoHist = actual.IdRecargo, IdPorcentajeHist = actual.IdPorcentaje, // Valores ANTERIORES + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Actualizada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, esAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, Fecha, TipoMovimiento, Cantidad, Remito, Observacion, Id_Precio AS IdPrecio, Id_Recargo AS IdRecargo, Id_Porcentaje AS IdPorcentaje + FROM dbo.dist_EntradasSalidas WHERE Id_Parte = @IdParteParam", + new { IdParteParam = idParte }, transaction); + if (actual == null) throw new KeyNotFoundException("Registro de Entrada/Salida no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_EntradasSalidas WHERE Id_Parte = @IdParteParam"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidas_H + (Id_Parte, Id_Publicacion, Id_Distribuidor, Fecha, TipoMovimiento, Cantidad, Remito, Observacion, Id_Precio, Id_Recargo, Id_Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPublicacionHist, @IdDistribuidorHist, @FechaHist, @TipoMovimientoHist, @CantidadHist, @RemitoHist, @ObservacionHist, @IdPrecioHist, @IdRecargoHist, @IdPorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdParteHist = actual.IdParte, IdPublicacionHist = actual.IdPublicacion, IdDistribuidorHist = actual.IdDistribuidor, + FechaHist = actual.Fecha, TipoMovimientoHist = actual.TipoMovimiento, CantidadHist = actual.Cantidad, RemitoHist = actual.Remito, ObservacionHist = actual.Observacion, + IdPrecioHist = actual.IdPrecio, IdRecargoHist = actual.IdRecargo, IdPorcentajeHist = actual.IdPorcentaje, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Eliminada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdParteParam = idParte }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaDistRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaDistRepository.cs new file mode 100644 index 0000000..75d29af --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaDistRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IEntradaSalidaDistRepository + { + Task> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento); + Task GetByIdAsync(int idParte); + Task CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction); + Task ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs index b0095c8..5fa7c91 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs @@ -1,10 +1,20 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; using System.Data; using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion { - public interface IPorcMonCanillaRepository - { - Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); - } + public interface IPorcMonCanillaRepository + { + Task> GetByPublicacionIdAsync(int idPublicacion); + Task GetByIdAsync(int idPorcMon); + Task GetActiveByPublicacionCanillaAndDateAsync(int idPublicacion, int idCanilla, DateTime fecha, IDbTransaction? transaction = null); + Task CreateAsync(PorcMonCanilla nuevoItem, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(PorcMonCanilla itemAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idPorcMon, int idUsuario, IDbTransaction transaction); + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + Task GetPreviousActiveAsync(int idPublicacion, int idCanilla, DateTime vigenciaDNuevo, IDbTransaction transaction); + } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs index 73c059d..723de17 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs @@ -1,9 +1,20 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; using System.Data; using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion - { - public interface IPorcPagoRepository { +{ + public interface IPorcPagoRepository + { + Task> GetByPublicacionIdAsync(int idPublicacion); + Task GetByIdAsync(int idPorcentaje); + Task GetActiveByPublicacionDistribuidorAndDateAsync(int idPublicacion, int idDistribuidor, DateTime fecha, IDbTransaction? transaction = null); + Task CreateAsync(PorcPago nuevoPorcPago, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(PorcPago porcPagoAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idPorcentaje, int idUsuario, IDbTransaction transaction); Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + Task GetPreviousActivePorcPagoAsync(int idPublicacion, int idDistribuidor, DateTime vigenciaDNuevo, IDbTransaction transaction); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs index 6f3f789..3eabb47 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs @@ -1,10 +1,19 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; using System.Data; using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion { - public interface IPubliSeccionRepository - { - Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); - } + public interface IPubliSeccionRepository + { + Task> GetByPublicacionIdAsync(int idPublicacion, bool? soloActivas = null); + Task GetByIdAsync(int idSeccion); + Task CreateAsync(PubliSeccion nuevaSeccion, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(PubliSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction); + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); // Ya existe + Task ExistsByNameInPublicacionAsync(string nombre, int idPublicacion, int? excludeIdSeccion = null); + Task IsInUseAsync(int idSeccion); // Verificar en bob_RegPublicaciones, bob_StockBobinas + } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs index 281dedf..5085ee1 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs @@ -1,3 +1,6 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; using System.Data; using System.Threading.Tasks; @@ -5,6 +8,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { public interface IRecargoZonaRepository { - Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + Task> GetByPublicacionIdAsync(int idPublicacion); + Task GetByIdAsync(int idRecargo); // Para obtener un recargo específico + Task GetActiveByPublicacionZonaAndDateAsync(int idPublicacion, int idZona, DateTime fecha, IDbTransaction? transaction = null); + Task CreateAsync(RecargoZona nuevoRecargo, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(RecargoZona recargoAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idRecargo, int idUsuario, IDbTransaction transaction); + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); // Ya existe + Task GetPreviousActiveRecargoAsync(int idPublicacion, int idZona, DateTime vigenciaDNuevo, IDbTransaction transaction); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ISalidaOtroDestinoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ISalidaOtroDestinoRepository.cs new file mode 100644 index 0000000..2ae5094 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ISalidaOtroDestinoRepository.cs @@ -0,0 +1,17 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface ISalidaOtroDestinoRepository + { + Task> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino); + Task GetByIdAsync(int idParte); + Task CreateAsync(SalidaOtroDestino nuevaSalida, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(SalidaOtroDestino salidaAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs index 2a0a333..cfcf9d9 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs @@ -1,56 +1,255 @@ using Dapper; -using System.Data; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Data; using System.Linq; +using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion { public class PorcMonCanillaRepository : IPorcMonCanillaRepository { - private readonly DbConnectionFactory _cf; private readonly ILogger _log; - public PorcMonCanillaRepository(DbConnectionFactory cf, ILogger log) { _cf = cf; _log = log; } + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public PorcMonCanillaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _cf = connectionFactory; + _log = logger; + } + + public async Task> GetByPublicacionIdAsync(int idPublicacion) + { + const string sql = @" + SELECT + pmc.Id_PorcMon AS IdPorcMon, pmc.Id_Publicacion AS IdPublicacion, pmc.Id_Canilla AS IdCanilla, + pmc.VigenciaD, pmc.VigenciaH, pmc.PorcMon, pmc.EsPorcentaje, + c.NomApe AS NomApeCanilla + FROM dbo.dist_PorcMonPagoCanilla pmc + INNER JOIN dbo.dist_dtCanillas c ON pmc.Id_Canilla = c.Id_Canilla + WHERE pmc.Id_Publicacion = @IdPublicacionParam + ORDER BY c.NomApe, pmc.VigenciaD DESC"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync( + sql, + (item, nomApe) => (item, nomApe), + new { IdPublicacionParam = idPublicacion }, + splitOn: "NomApeCanilla" + ); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener PorcMonCanilla por Publicacion ID: {IdPublicacion}", idPublicacion); + return Enumerable.Empty<(PorcMonCanilla, string)>(); + } + } + + public async Task GetByIdAsync(int idPorcMon) + { + const string sql = @" + SELECT + Id_PorcMon AS IdPorcMon, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + VigenciaD, VigenciaH, PorcMon, EsPorcentaje + FROM dbo.dist_PorcMonPagoCanilla + WHERE Id_PorcMon = @IdPorcMonParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdPorcMonParam = idPorcMon }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener PorcMonCanilla por ID: {IdPorcMon}", idPorcMon); + return null; + } + } + + public async Task GetActiveByPublicacionCanillaAndDateAsync(int idPublicacion, int idCanilla, DateTime fecha, IDbTransaction? transaction = null) + { + const string sql = @" + SELECT TOP 1 + Id_PorcMon AS IdPorcMon, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + VigenciaD, VigenciaH, PorcMon, EsPorcentaje + FROM dbo.dist_PorcMonPagoCanilla + WHERE Id_Publicacion = @IdPublicacionParam AND Id_Canilla = @IdCanillaParam + AND VigenciaD <= @FechaParam + AND (VigenciaH IS NULL OR VigenciaH >= @FechaParam) + ORDER BY VigenciaD DESC;"; + + var cn = transaction?.Connection ?? _cf.CreateConnection(); + bool ownConnection = transaction == null; + PorcMonCanilla? result = null; + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); + result = await cn.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, IdCanillaParam = idCanilla, FechaParam = fecha.Date }, + transaction); + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; + } + + public async Task GetPreviousActiveAsync(int idPublicacion, int idCanilla, DateTime vigenciaDNuevo, IDbTransaction transaction) + { + const string sql = @" + SELECT TOP 1 + Id_PorcMon AS IdPorcMon, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + VigenciaD, VigenciaH, PorcMon, EsPorcentaje + FROM dbo.dist_PorcMonPagoCanilla + WHERE Id_Publicacion = @IdPublicacionParam AND Id_Canilla = @IdCanillaParam + AND VigenciaD < @VigenciaDNuevoParam + AND VigenciaH IS NULL + ORDER BY VigenciaD DESC;"; + return await transaction.Connection!.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, IdCanillaParam = idCanilla, VigenciaDNuevoParam = vigenciaDNuevo.Date }, + transaction); + } + + public async Task CreateAsync(PorcMonCanilla nuevoItem, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_PorcMonPagoCanilla (Id_Publicacion, Id_Canilla, VigenciaD, VigenciaH, PorcMon, EsPorcentaje) + OUTPUT INSERTED.Id_PorcMon AS IdPorcMon, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Canilla AS IdCanilla, + INSERTED.VigenciaD, INSERTED.VigenciaH, INSERTED.PorcMon, INSERTED.EsPorcentaje + VALUES (@IdPublicacion, @IdCanilla, @VigenciaD, @VigenciaH, @PorcMon, @EsPorcentaje);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_PorcMonPagoCanilla_H (Id_PorcMon, Id_Publicacion, Id_Canilla, VigenciaD, VigenciaH, PorcMon, EsPorcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcMonParam, @IdPublicacionParam, @IdCanillaParam, @VigenciaDParam, @VigenciaHParam, @PorcMonParam, @EsPorcentajeParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoItem, transaction); + if (inserted == null || inserted.IdPorcMon == 0) throw new DataException("Error al crear PorcMonCanilla o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdPorcMonParam = inserted.IdPorcMon, + IdPublicacionParam = inserted.IdPublicacion, + IdCanillaParam = inserted.IdCanilla, + VigenciaDParam = inserted.VigenciaD, + VigenciaHParam = inserted.VigenciaH, + PorcMonParam = inserted.PorcMon, + EsPorcentajeParam = inserted.EsPorcentaje, + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(PorcMonCanilla itemAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_PorcMon AS IdPorcMon, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + VigenciaD, VigenciaH, PorcMon, EsPorcentaje + FROM dbo.dist_PorcMonPagoCanilla WHERE Id_PorcMon = @IdPorcMonParam", + new { IdPorcMonParam = itemAActualizar.IdPorcMon }, transaction); + if (actual == null) throw new KeyNotFoundException("Registro PorcMonCanilla no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_PorcMonPagoCanilla SET + PorcMon = @PorcMon, EsPorcentaje = @EsPorcentaje, VigenciaH = @VigenciaH + WHERE Id_PorcMon = @IdPorcMon;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_PorcMonPagoCanilla_H (Id_PorcMon, Id_Publicacion, Id_Canilla, VigenciaD, VigenciaH, PorcMon, EsPorcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcMonParam, @IdPublicacionParam, @IdCanillaParam, @VigenciaDParam, @VigenciaHParam, @PorcMonParam, @EsPorcentajeParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdPorcMonParam = actual.IdPorcMon, + IdPublicacionParam = actual.IdPublicacion, + IdCanillaParam = actual.IdCanilla, + VigenciaDParam = actual.VigenciaD, + VigenciaHParam = actual.VigenciaH, // Valor ANTERIOR + PorcMonParam = actual.PorcMon, // Valor ANTERIOR + EsPorcentajeParam = actual.EsPorcentaje, // Valor ANTERIOR + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Actualizado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, itemAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idPorcMon, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_PorcMon AS IdPorcMon, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + VigenciaD, VigenciaH, PorcMon, EsPorcentaje + FROM dbo.dist_PorcMonPagoCanilla WHERE Id_PorcMon = @IdPorcMonParam", + new { IdPorcMonParam = idPorcMon }, transaction); + if (actual == null) throw new KeyNotFoundException("Registro PorcMonCanilla no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_PorcMonPagoCanilla WHERE Id_PorcMon = @IdPorcMonParam"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_PorcMonPagoCanilla_H (Id_PorcMon, Id_Publicacion, Id_Canilla, VigenciaD, VigenciaH, PorcMon, EsPorcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcMonParam, @IdPublicacionParam, @IdCanillaParam, @VigenciaDParam, @VigenciaHParam, @PorcMonParam, @EsPorcentajeParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdPorcMonParam = actual.IdPorcMon, + IdPublicacionParam = actual.IdPublicacion, + IdCanillaParam = actual.IdCanilla, + VigenciaDParam = actual.VigenciaD, + VigenciaHParam = actual.VigenciaH, + PorcMonParam = actual.PorcMon, + EsPorcentajeParam = actual.EsPorcentaje, + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdPorcMonParam = idPorcMon }, transaction); + return rowsAffected == 1; + } public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) { - const string selectSql = "SELECT * FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdPublicacion"; - var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacion = idPublicacion }, transaction); + const string selectSql = @" + SELECT Id_PorcMon AS IdPorcMon, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + VigenciaD, VigenciaH, PorcMon, EsPorcentaje + FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdPublicacionParam"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacionParam = idPublicacion }, transaction); if (itemsToDelete.Any()) { const string insertHistoricoSql = @" INSERT INTO dbo.dist_PorcMonPagoCanilla_H (Id_PorcMon, Id_Publicacion, Id_Canilla, VigenciaD, VigenciaH, PorcMon, EsPorcentaje, Id_Usuario, FechaMod, TipoMod) - VALUES (@Id_PorcMon, @Id_Publicacion, @Id_Canilla, @VigenciaD, @VigenciaH, @PorcMon, @EsPorcentaje, @Id_Usuario, @FechaMod, @TipoMod);"; - + VALUES (@IdPorcMonParam, @IdPublicacionParam, @IdCanillaParam, @VigenciaDParam, @VigenciaHParam, @PorcMonParam, @EsPorcentajeParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; foreach (var item in itemsToDelete) { await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new { - Id_PorcMon = item.IdPorcMon, // Mapeo de propiedad a parámetro SQL - Id_Publicacion = item.IdPublicacion, - Id_Canilla = item.IdCanilla, - item.VigenciaD, - item.VigenciaH, - item.PorcMon, - item.EsPorcentaje, - Id_Usuario = idUsuarioAuditoria, - FechaMod = DateTime.Now, - TipoMod = "Eliminado (Cascada)" + IdPorcMonParam = item.IdPorcMon, + IdPublicacionParam = item.IdPublicacion, + IdCanillaParam = item.IdCanilla, + VigenciaDParam = item.VigenciaD, + VigenciaHParam = item.VigenciaH, + PorcMonParam = item.PorcMon, + EsPorcentajeParam = item.EsPorcentaje, + IdUsuarioParam = idUsuarioAuditoria, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminado (Cascada)" }, transaction); } } - - const string deleteSql = "DELETE FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdPublicacion"; + const string deleteSql = "DELETE FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdPublicacionParam"; try { - await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacion = idPublicacion }, transaction); + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacionParam = idPublicacion }, transaction: transaction); return true; } - catch (System.Exception e) + catch (System.Exception ex) { - _log.LogError(e, "Error al eliminar PorcMonCanilla por IdPublicacion: {idPublicacion}", idPublicacion); + _log.LogError(ex, "Error al eliminar PorcMonCanilla por IdPublicacion: {idPublicacion}", idPublicacion); throw; } } diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs index 8c2eeb8..6617b9e 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs @@ -1,54 +1,249 @@ -using Dapper; using System.Data; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Dapper; using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Data; using System.Linq; +using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion { public class PorcPagoRepository : IPorcPagoRepository { - private readonly DbConnectionFactory _cf; private readonly ILogger _log; - public PorcPagoRepository(DbConnectionFactory cf, ILogger log) { _cf = cf; _log = log; } + private readonly DbConnectionFactory _cf; // Renombrado para brevedad + private readonly ILogger _log; // Renombrado para brevedad + + public PorcPagoRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _cf = connectionFactory; + _log = logger; + } + + public async Task> GetByPublicacionIdAsync(int idPublicacion) + { + const string sql = @" + SELECT + pp.Id_Porcentaje AS IdPorcentaje, pp.Id_Publicacion AS IdPublicacion, pp.Id_Distribuidor AS IdDistribuidor, + pp.VigenciaD, pp.VigenciaH, pp.Porcentaje, + d.Nombre AS NombreDistribuidor + FROM dbo.dist_PorcPago pp + INNER JOIN dbo.dist_dtDistribuidores d ON pp.Id_Distribuidor = d.Id_Distribuidor + WHERE pp.Id_Publicacion = @IdPublicacionParam + ORDER BY d.Nombre, pp.VigenciaD DESC"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync( + sql, + (porcPago, nombreDist) => (porcPago, nombreDist), + new { IdPublicacionParam = idPublicacion }, + splitOn: "NombreDistribuidor" + ); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Porcentajes de Pago por Publicacion ID: {IdPublicacion}", idPublicacion); + return Enumerable.Empty<(PorcPago, string)>(); + } + } + + public async Task GetByIdAsync(int idPorcentaje) + { + const string sql = @" + SELECT + Id_Porcentaje AS IdPorcentaje, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + VigenciaD, VigenciaH, Porcentaje + FROM dbo.dist_PorcPago + WHERE Id_Porcentaje = @IdPorcentajeParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdPorcentajeParam = idPorcentaje }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Porcentaje de Pago por ID: {IdPorcentaje}", idPorcentaje); + return null; + } + } + + public async Task GetActiveByPublicacionDistribuidorAndDateAsync(int idPublicacion, int idDistribuidor, DateTime fecha, IDbTransaction? transaction = null) + { + const string sql = @" + SELECT TOP 1 + Id_Porcentaje AS IdPorcentaje, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + VigenciaD, VigenciaH, Porcentaje + FROM dbo.dist_PorcPago + WHERE Id_Publicacion = @IdPublicacionParam AND Id_Distribuidor = @IdDistribuidorParam + AND VigenciaD <= @FechaParam + AND (VigenciaH IS NULL OR VigenciaH >= @FechaParam) + ORDER BY VigenciaD DESC;"; + + var cn = transaction?.Connection ?? _cf.CreateConnection(); + bool ownConnection = transaction == null; + PorcPago? result = null; + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); + result = await cn.QuerySingleOrDefaultAsync(sql, new { IdPublicacionParam = idPublicacion, IdDistribuidorParam = idDistribuidor, FechaParam = fecha.Date }, transaction); + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; + } + + public async Task GetPreviousActivePorcPagoAsync(int idPublicacion, int idDistribuidor, DateTime vigenciaDNuevo, IDbTransaction transaction) + { + const string sql = @" + SELECT TOP 1 + Id_Porcentaje AS IdPorcentaje, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + VigenciaD, VigenciaH, Porcentaje + FROM dbo.dist_PorcPago + WHERE Id_Publicacion = @IdPublicacionParam AND Id_Distribuidor = @IdDistribuidorParam + AND VigenciaD < @VigenciaDNuevoParam + AND VigenciaH IS NULL + ORDER BY VigenciaD DESC;"; + return await transaction.Connection!.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, IdDistribuidorParam = idDistribuidor, VigenciaDNuevoParam = vigenciaDNuevo.Date }, + transaction); + } + + public async Task CreateAsync(PorcPago nuevoPorcPago, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_PorcPago (Id_Publicacion, Id_Distribuidor, VigenciaD, VigenciaH, Porcentaje) + OUTPUT INSERTED.Id_Porcentaje AS IdPorcentaje, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Distribuidor AS IdDistribuidor, + INSERTED.VigenciaD, INSERTED.VigenciaH, INSERTED.Porcentaje + VALUES (@IdPublicacion, @IdDistribuidor, @VigenciaD, @VigenciaH, @Porcentaje);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_PorcPago_H (Id_Porcentaje, Id_Publicacion, Id_Distribuidor, VigenciaD, VigenciaH, Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcentajeHist, @IdPublicacionHist, @IdDistribuidorHist, @VigenciaDHist, @VigenciaHHist, @PorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoPorcPago, transaction); + if (inserted == null || inserted.IdPorcentaje == 0) throw new DataException("Error al crear porcentaje de pago o al obtener ID."); + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdPorcentajeHist = inserted.IdPorcentaje, + IdPublicacionHist = inserted.IdPublicacion, + IdDistribuidorHist = inserted.IdDistribuidor, + VigenciaDHist = inserted.VigenciaD, + VigenciaHHist = inserted.VigenciaH, + PorcentajeHist = inserted.Porcentaje, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(PorcPago porcPagoAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Porcentaje AS IdPorcentaje, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + VigenciaD, VigenciaH, Porcentaje + FROM dbo.dist_PorcPago WHERE Id_Porcentaje = @IdPorcentajeParam", + new { IdPorcentajeParam = porcPagoAActualizar.IdPorcentaje }, transaction); + if (actual == null) throw new KeyNotFoundException("Porcentaje de pago no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_PorcPago SET + Porcentaje = @Porcentaje, VigenciaH = @VigenciaH + WHERE Id_Porcentaje = @IdPorcentaje;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_PorcPago_H (Id_Porcentaje, Id_Publicacion, Id_Distribuidor, VigenciaD, VigenciaH, Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcentajeHist, @IdPublicacionHist, @IdDistribuidorHist, @VigenciaDHist, @VigenciaHHist, @PorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdPorcentajeHist = actual.IdPorcentaje, + IdPublicacionHist = actual.IdPublicacion, + IdDistribuidorHist = actual.IdDistribuidor, + VigenciaDHist = actual.VigenciaD, + VigenciaHHist = actual.VigenciaH, // Valor ANTERIOR de VigenciaH + PorcentajeHist = actual.Porcentaje, // Valor ANTERIOR de Porcentaje + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Actualizado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, porcPagoAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idPorcentaje, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Porcentaje AS IdPorcentaje, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + VigenciaD, VigenciaH, Porcentaje + FROM dbo.dist_PorcPago WHERE Id_Porcentaje = @IdPorcentajeParam", + new { IdPorcentajeParam = idPorcentaje }, transaction); + if (actual == null) throw new KeyNotFoundException("Porcentaje de pago no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_PorcPago WHERE Id_Porcentaje = @IdPorcentajeParam"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_PorcPago_H (Id_Porcentaje, Id_Publicacion, Id_Distribuidor, VigenciaD, VigenciaH, Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcentajeHist, @IdPublicacionHist, @IdDistribuidorHist, @VigenciaDHist, @VigenciaHHist, @PorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdPorcentajeHist = actual.IdPorcentaje, + IdPublicacionHist = actual.IdPublicacion, + IdDistribuidorHist = actual.IdDistribuidor, + VigenciaDHist = actual.VigenciaD, + VigenciaHHist = actual.VigenciaH, + PorcentajeHist = actual.Porcentaje, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdPorcentajeParam = idPorcentaje }, transaction); + return rowsAffected == 1; + } public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) { - const string selectSql = "SELECT * FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdPublicacion"; - var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacion = idPublicacion }, transaction); + const string selectSql = @" + SELECT Id_Porcentaje AS IdPorcentaje, Id_Publicacion AS IdPublicacion, Id_Distribuidor AS IdDistribuidor, + VigenciaD, VigenciaH, Porcentaje + FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdPublicacionParam"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacionParam = idPublicacion }, transaction); if (itemsToDelete.Any()) { const string insertHistoricoSql = @" INSERT INTO dbo.dist_PorcPago_H (Id_Porcentaje, Id_Publicacion, Id_Distribuidor, VigenciaD, VigenciaH, Porcentaje, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPorcentaje, @IdPublicacion, @IdDistribuidor, @VigenciaD, @VigenciaH, @Porcentaje, @Id_Usuario, @FechaMod, @TipoMod);"; - + VALUES (@IdPorcentajeHist, @IdPublicacionHist, @IdDistribuidorHist, @VigenciaDHist, @VigenciaHHist, @PorcentajeHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; foreach (var item in itemsToDelete) { await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new { - item.IdPorcentaje, // Mapea a @Id_Porcentaje si usas nombres con _ en SQL - item.IdPublicacion, - item.IdDistribuidor, - item.VigenciaD, - item.VigenciaH, - item.Porcentaje, - Id_Usuario = idUsuarioAuditoria, - FechaMod = DateTime.Now, - TipoMod = "Eliminado (Cascada)" + IdPorcentajeHist = item.IdPorcentaje, + IdPublicacionHist = item.IdPublicacion, + IdDistribuidorHist = item.IdDistribuidor, + VigenciaDHist = item.VigenciaD, + VigenciaHHist = item.VigenciaH, + PorcentajeHist = item.Porcentaje, + IdUsuarioHist = idUsuarioAuditoria, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminado (Cascada)" }, transaction); } } - - const string deleteSql = "DELETE FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdPublicacion"; + const string deleteSql = "DELETE FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdPublicacionParam"; try { - await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacion = idPublicacion }, transaction); + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacionParam = idPublicacion }, transaction: transaction); return true; } - catch (System.Exception e) + catch (System.Exception ex) { - _log.LogError(e, "Error al eliminar PorcPago por IdPublicacion: {idPublicacion}", idPublicacion); + _log.LogError(ex, "Error al eliminar PorcPago por IdPublicacion: {idPublicacion}", idPublicacion); throw; } } diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs index ee58619..c1aa02f 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs @@ -1,6 +1,7 @@ using Dapper; using GestionIntegral.Api.Models.Distribucion; using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -21,66 +22,127 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task> GetByPublicacionIdAsync(int idPublicacion) { - const string sql = "SELECT * FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacion ORDER BY VigenciaD DESC"; - using var connection = _connectionFactory.CreateConnection(); - return await connection.QueryAsync(sql, new { IdPublicacion = idPublicacion }); + const string sql = @" + SELECT + Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo + FROM dbo.dist_Precios + WHERE Id_Publicacion = @IdPublicacionParam + ORDER BY VigenciaD DESC"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sql, new { IdPublicacionParam = idPublicacion }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener precios por IdPublicacion: {IdPublicacion}", idPublicacion); + return Enumerable.Empty(); + } } public async Task GetByIdAsync(int idPrecio) { - const string sql = "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio"; - using var connection = _connectionFactory.CreateConnection(); - return await connection.QuerySingleOrDefaultAsync(sql, new { IdPrecio = idPrecio }); + const string sql = @" + SELECT + Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo + FROM dbo.dist_Precios + WHERE Id_Precio = @IdPrecioParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdPrecioParam = idPrecio }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener precio por ID: {idPrecio}", idPrecio); + return null; + } } public async Task GetActiveByPublicacionAndDateAsync(int idPublicacion, DateTime fecha, IDbTransaction? transaction = null) { - // Obtiene el precio vigente para una publicación en una fecha específica const string sql = @" - SELECT TOP 1 * FROM dbo.dist_Precios - WHERE Id_Publicacion = @IdPublicacion - AND VigenciaD <= @Fecha - AND (VigenciaH IS NULL OR VigenciaH >= @Fecha) - ORDER BY VigenciaD DESC;"; // Por si hay solapamientos incorrectos, tomar el más reciente + SELECT TOP 1 + Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo + FROM dbo.dist_Precios + WHERE Id_Publicacion = @IdPublicacionParam AND VigenciaD <= @FechaParam + AND (VigenciaH IS NULL OR VigenciaH >= @FechaParam) + ORDER BY VigenciaD DESC;"; var cn = transaction?.Connection ?? _connectionFactory.CreateConnection(); - return await cn.QuerySingleOrDefaultAsync(sql, new { IdPublicacion = idPublicacion, Fecha = fecha }, transaction); + bool ownConnection = transaction == null; + Precio? result = null; + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) + { + await dbConn.OpenAsync(); + } + result = await cn.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, FechaParam = fecha.Date }, + transaction); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en GetActiveByPublicacionAndDateAsync. IdPublicacion: {IdPublicacion}, Fecha: {Fecha}", idPublicacion, fecha); + throw; // Re-lanzar para que el servicio lo maneje si es necesario + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) + { + await dbConnClose.CloseAsync(); + } + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; } public async Task GetPreviousActivePriceAsync(int idPublicacion, DateTime vigenciaDNuevo, IDbTransaction transaction) { - // Busca el último precio activo antes de la vigenciaD del nuevo precio, que no tenga VigenciaH o cuya VigenciaH sea mayor o igual a la nueva VigenciaD - 1 día - // y que no sea el mismo periodo que se está por crear/actualizar (si tuviera un ID). const string sql = @" - SELECT TOP 1 * + SELECT TOP 1 + Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo FROM dbo.dist_Precios - WHERE Id_Publicacion = @IdPublicacion - AND VigenciaD < @VigenciaDNuevo + WHERE Id_Publicacion = @IdPublicacionParam + AND VigenciaD < @VigenciaDNuevoParam AND VigenciaH IS NULL ORDER BY VigenciaD DESC;"; - return await transaction.Connection!.QuerySingleOrDefaultAsync(sql, new { IdPublicacion = idPublicacion, VigenciaDNuevo = vigenciaDNuevo }, transaction); + return await transaction.Connection!.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, VigenciaDNuevoParam = vigenciaDNuevo.Date }, + transaction); } - public async Task CreateAsync(Precio nuevoPrecio, int idUsuario, IDbTransaction transaction) { const string sqlInsert = @" INSERT INTO dbo.dist_Precios (Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo) - OUTPUT INSERTED.* + OUTPUT INSERTED.Id_Precio AS IdPrecio, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.VigenciaD, INSERTED.VigenciaH, + INSERTED.Lunes, INSERTED.Martes, INSERTED.Miercoles, INSERTED.Jueves, INSERTED.Viernes, INSERTED.Sabado, INSERTED.Domingo VALUES (@IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo);"; + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoPrecio, transaction); + if (inserted == null || inserted.IdPrecio == 0) throw new DataException("Error al crear precio o al obtener el ID generado."); + const string sqlInsertHistorico = @" INSERT INTO dbo.dist_Precios_H (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; - - var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoPrecio, transaction); - if (inserted == null) throw new DataException("Error al crear precio."); + VALUES (@IdPrecioHist, @IdPublicacionHist, @VigenciaDHist, @VigenciaHHist, @LunesHist, @MartesHist, @MiercolesHist, @JuevesHist, @ViernesHist, @SabadoHist, @DomingoHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { - inserted.IdPrecio, inserted.IdPublicacion, inserted.VigenciaD, inserted.VigenciaH, - inserted.Lunes, inserted.Martes, inserted.Miercoles, inserted.Jueves, inserted.Viernes, inserted.Sabado, inserted.Domingo, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creado" + IdPrecioHist = inserted.IdPrecio, + IdPublicacionHist = inserted.IdPublicacion, + VigenciaDHist = inserted.VigenciaD, + VigenciaHHist = inserted.VigenciaH, + LunesHist = inserted.Lunes, MartesHist = inserted.Martes, MiercolesHist = inserted.Miercoles, JuevesHist = inserted.Jueves, + ViernesHist = inserted.Viernes, SabadoHist = inserted.Sabado, DomingoHist = inserted.Domingo, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Creado" }, transaction); return inserted; } @@ -88,28 +150,32 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task UpdateAsync(Precio precioAActualizar, int idUsuario, IDbTransaction transaction) { var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio", - new { precioAActualizar.IdPrecio }, transaction); + @"SELECT Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo + FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecioParam", // Usar parámetro con alias + new { IdPrecioParam = precioAActualizar.IdPrecio }, transaction); if (actual == null) throw new KeyNotFoundException("Precio no encontrado."); - const string sqlUpdate = @" UPDATE dbo.dist_Precios SET VigenciaH = @VigenciaH, Lunes = @Lunes, Martes = @Martes, Miercoles = @Miercoles, Jueves = @Jueves, Viernes = @Viernes, Sabado = @Sabado, Domingo = @Domingo - -- No se permite cambiar IdPublicacion ni VigenciaD de un registro de precio existente WHERE Id_Precio = @IdPrecio;"; const string sqlInsertHistorico = @" INSERT INTO dbo.dist_Precios_H (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdPrecioHist, @IdPublicacionHist, @VigenciaDHist, @VigenciaHHist, @LunesHist, @MartesHist, @MiercolesHist, @JuevesHist, @ViernesHist, @SabadoHist, @DomingoHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { - actual.IdPrecio, actual.IdPublicacion, actual.VigenciaD, // VigenciaD actual para el historial - VigenciaH = actual.VigenciaH, // VigenciaH actual para el historial - Lunes = actual.Lunes, Martes = actual.Martes, Miercoles = actual.Miercoles, Jueves = actual.Jueves, - Viernes = actual.Viernes, Sabado = actual.Sabado, Domingo = actual.Domingo, // Precios actuales para el historial - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizado" // O "Cerrado" si solo se actualiza VigenciaH + IdPrecioHist = actual.IdPrecio, + IdPublicacionHist = actual.IdPublicacion, + VigenciaDHist = actual.VigenciaD, + VigenciaHHist = actual.VigenciaH, + LunesHist = actual.Lunes, MartesHist = actual.Martes, MiercolesHist = actual.Miercoles, JuevesHist = actual.Jueves, + ViernesHist = actual.Viernes, SabadoHist = actual.Sabado, DomingoHist = actual.Domingo, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Actualizado" }, transaction); var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, precioAActualizar, transaction); @@ -119,53 +185,71 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task DeleteAsync(int idPrecio, int idUsuario, IDbTransaction transaction) { var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio", new { IdPrecio = idPrecio }, transaction); + @"SELECT Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo + FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecioParam", new { IdPrecioParam = idPrecio }, transaction); if (actual == null) throw new KeyNotFoundException("Precio no encontrado para eliminar."); - const string sqlDelete = "DELETE FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio"; + const string sqlDelete = "DELETE FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecioParam"; // Usar parámetro con alias const string sqlInsertHistorico = @" INSERT INTO dbo.dist_Precios_H (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdPrecioHist, @IdPublicacionHist, @VigenciaDHist, @VigenciaHHist, @LunesHist, @MartesHist, @MiercolesHist, @JuevesHist, @ViernesHist, @SabadoHist, @DomingoHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { - actual.IdPrecio, actual.IdPublicacion, actual.VigenciaD, actual.VigenciaH, - actual.Lunes, actual.Martes, actual.Miercoles, actual.Jueves, actual.Viernes, actual.Sabado, actual.Domingo, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Eliminado" + IdPrecioHist = actual.IdPrecio, + IdPublicacionHist = actual.IdPublicacion, + VigenciaDHist = actual.VigenciaD, + VigenciaHHist = actual.VigenciaH, + LunesHist = actual.Lunes, MartesHist = actual.Martes, MiercolesHist = actual.Miercoles, JuevesHist = actual.Jueves, + ViernesHist = actual.Viernes, SabadoHist = actual.Sabado, DomingoHist = actual.Domingo, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminado" }, transaction); - var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdPrecio = idPrecio }, transaction); + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdPrecioParam = idPrecio }, transaction); // Usar parámetro con alias return rowsAffected == 1; } - public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) // MODIFICADO: Recibe idUsuarioAuditoria - { - const string selectPrecios = "SELECT * FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacion"; - var preciosAEliminar = await transaction.Connection!.QueryAsync(selectPrecios, new { IdPublicacion = idPublicacion }, transaction); + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + { + const string selectSql = @" + SELECT + Id_Precio AS IdPrecio, Id_Publicacion AS IdPublicacion, VigenciaD, VigenciaH, + Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo + FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacionParam"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacionParam = idPublicacion }, transaction); - const string sqlInsertHistorico = @" - INSERT INTO dbo.dist_Precios_H - (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; - - foreach (var precio in preciosAEliminar) + if (itemsToDelete.Any()) { - await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + const string insertHistoricoSql = @" + INSERT INTO dbo.dist_Precios_H + (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPrecioHist, @IdPublicacionHist, @VigenciaDHist, @VigenciaHHist, @LunesHist, @MartesHist, @MiercolesHist, @JuevesHist, @ViernesHist, @SabadoHist, @DomingoHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + foreach (var item in itemsToDelete) { - precio.IdPrecio, precio.IdPublicacion, precio.VigenciaD, precio.VigenciaH, - precio.Lunes, precio.Martes, precio.Miercoles, precio.Jueves, precio.Viernes, precio.Sabado, precio.Domingo, - Id_Usuario = idUsuarioAuditoria, // MODIFICADO: Usar el idUsuarioAuditoria pasado - FechaMod = DateTime.Now, TipoMod = "Eliminado (Cascada)" - }, transaction); + await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new + { + IdPrecioHist = item.IdPrecio, + IdPublicacionHist = item.IdPublicacion, + VigenciaDHist = item.VigenciaD, + VigenciaHHist = item.VigenciaH, + LunesHist = item.Lunes, MartesHist = item.Martes, MiercolesHist = item.Miercoles, JuevesHist = item.Jueves, + ViernesHist = item.Viernes, SabadoHist = item.Sabado, DomingoHist = item.Domingo, + IdUsuarioHist = idUsuarioAuditoria, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminado (Cascada)" + }, transaction); + } } - - const string sql = "DELETE FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacion"; + const string deleteSql = "DELETE FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacionParam"; try { - var rowsAffected = await transaction.Connection!.ExecuteAsync(sql, new { IdPublicacion = idPublicacion }, transaction: transaction); - // No necesitamos devolver rowsAffected >= 0 si la lógica del servicio ya valida si debe haber registros - return true; // Indica que la operación de borrado (incluyendo 0 filas) se intentó + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacionParam = idPublicacion }, transaction: transaction); + return true; } catch (System.Exception ex) { diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs index 2bc5b46..1e7a27d 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs @@ -1,53 +1,237 @@ using Dapper; -using System.Data; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Data; using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion { public class PubliSeccionRepository : IPubliSeccionRepository { - private readonly DbConnectionFactory _cf; private readonly ILogger _log; - public PubliSeccionRepository(DbConnectionFactory cf, ILogger log) { _cf = cf; _log = log; } + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public PubliSeccionRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _cf = connectionFactory; + _log = logger; + } + + public async Task> GetByPublicacionIdAsync(int idPublicacion, bool? soloActivas = null) + { + var sqlBuilder = new StringBuilder(@" + SELECT + Id_Seccion AS IdSeccion, Id_Publicacion AS IdPublicacion, Nombre, Estado + FROM dbo.dist_dtPubliSecciones + WHERE Id_Publicacion = @IdPublicacionParam"); + + if (soloActivas.HasValue) + { + sqlBuilder.Append(soloActivas.Value ? " AND Estado = 1" : " AND Estado = 0"); + } + sqlBuilder.Append(" ORDER BY Nombre;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), new { IdPublicacionParam = idPublicacion }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Secciones por Publicacion ID: {IdPublicacion}", idPublicacion); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idSeccion) + { + const string sql = @" + SELECT + Id_Seccion AS IdSeccion, Id_Publicacion AS IdPublicacion, Nombre, Estado + FROM dbo.dist_dtPubliSecciones + WHERE Id_Seccion = @IdSeccionParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdSeccionParam = idSeccion }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Sección por ID: {IdSeccion}", idSeccion); + return null; + } + } + + public async Task ExistsByNameInPublicacionAsync(string nombre, int idPublicacion, int? excludeIdSeccion = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtPubliSecciones WHERE Nombre = @NombreParam AND Id_Publicacion = @IdPublicacionParam"); + var parameters = new DynamicParameters(); + parameters.Add("NombreParam", nombre); + parameters.Add("IdPublicacionParam", idPublicacion); + if (excludeIdSeccion.HasValue) + { + sqlBuilder.Append(" AND Id_Seccion != @ExcludeIdSeccionParam"); + parameters.Add("ExcludeIdSeccionParam", excludeIdSeccion.Value); + } + try + { + using var connection = _cf.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error en ExistsByNameInPublicacionAsync. Nombre: {Nombre}, PubID: {IdPublicacion}", nombre, idPublicacion); + return true; + } + } + + public async Task IsInUseAsync(int idSeccion) + { + // Verificar en bob_RegPublicaciones y bob_StockBobinas (si Id_Seccion se usa allí) + using var connection = _cf.CreateConnection(); + string[] checkQueries = { + "SELECT TOP 1 1 FROM dbo.bob_RegPublicaciones WHERE Id_Seccion = @IdSeccionParam", + "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_Seccion = @IdSeccionParam" + }; + try + { + foreach (var query in checkQueries) + { + if (await connection.ExecuteScalarAsync(query, new { IdSeccionParam = idSeccion }) == 1) return true; + } + return false; + } + catch (Exception ex) + { + _log.LogError(ex, "Error en IsInUseAsync para PubliSeccion ID: {idSeccion}", idSeccion); + return true; + } + } + + public async Task CreateAsync(PubliSeccion nuevaSeccion, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_dtPubliSecciones (Id_Publicacion, Nombre, Estado) + OUTPUT INSERTED.Id_Seccion AS IdSeccion, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Nombre, INSERTED.Estado + VALUES (@IdPublicacion, @Nombre, @Estado);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtPubliSecciones_H (Id_Seccion, Id_Publicacion, Nombre, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdSeccionParam, @IdPublicacionParam, @NombreParam, @EstadoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaSeccion, transaction); + if (inserted == null || inserted.IdSeccion == 0) throw new DataException("Error al crear sección o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdSeccionParam = inserted.IdSeccion, + IdPublicacionParam = inserted.IdPublicacion, + NombreParam = inserted.Nombre, + EstadoParam = inserted.Estado, + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Creada" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(PubliSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Seccion AS IdSeccion, Id_Publicacion AS IdPublicacion, Nombre, Estado + FROM dbo.dist_dtPubliSecciones WHERE Id_Seccion = @IdSeccionParam", + new { IdSeccionParam = seccionAActualizar.IdSeccion }, transaction); + if (actual == null) throw new KeyNotFoundException("Sección no encontrada."); + + const string sqlUpdate = @" + UPDATE dbo.dist_dtPubliSecciones SET + Nombre = @Nombre, Estado = @Estado + WHERE Id_Seccion = @IdSeccion;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtPubliSecciones_H (Id_Seccion, Id_Publicacion, Nombre, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdSeccionParam, @IdPublicacionParam, @NombreParam, @EstadoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdSeccionParam = actual.IdSeccion, + IdPublicacionParam = actual.IdPublicacion, // Tomar de 'actual' ya que no se modifica + NombreParam = actual.Nombre, // Valor ANTERIOR + EstadoParam = actual.Estado, // Valor ANTERIOR + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Actualizada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, seccionAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Seccion AS IdSeccion, Id_Publicacion AS IdPublicacion, Nombre, Estado + FROM dbo.dist_dtPubliSecciones WHERE Id_Seccion = @IdSeccionParam", + new { IdSeccionParam = idSeccion }, transaction); + if (actual == null) throw new KeyNotFoundException("Sección no encontrada para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_dtPubliSecciones WHERE Id_Seccion = @IdSeccionParam"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtPubliSecciones_H (Id_Seccion, Id_Publicacion, Nombre, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdSeccionParam, @IdPublicacionParam, @NombreParam, @EstadoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdSeccionParam = actual.IdSeccion, + IdPublicacionParam = actual.IdPublicacion, + NombreParam = actual.Nombre, + EstadoParam = actual.Estado, + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdSeccionParam = idSeccion }, transaction); + return rowsAffected == 1; + } public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) { - const string selectSql = "SELECT * FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdPublicacion"; - var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacion = idPublicacion }, transaction); + const string selectSql = @" + SELECT Id_Seccion AS IdSeccion, Id_Publicacion AS IdPublicacion, Nombre, Estado + FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdPublicacionParam"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacionParam = idPublicacion }, transaction); if (itemsToDelete.Any()) { const string insertHistoricoSql = @" INSERT INTO dbo.dist_dtPubliSecciones_H (Id_Seccion, Id_Publicacion, Nombre, Estado, Id_Usuario, FechaMod, TipoMod) - VALUES (@Id_Seccion, @Id_Publicacion, @Nombre, @Estado, @Id_Usuario, @FechaMod, @TipoMod);"; - + VALUES (@IdSeccionParam, @IdPublicacionParam, @NombreParam, @EstadoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; foreach (var item in itemsToDelete) { await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new { - Id_Seccion = item.IdSeccion, // Mapeo de propiedad a parámetro SQL - Id_Publicacion = item.IdPublicacion, - item.Nombre, - item.Estado, - Id_Usuario = idUsuarioAuditoria, - FechaMod = DateTime.Now, - TipoMod = "Eliminado (Cascada)" + IdSeccionParam = item.IdSeccion, + IdPublicacionParam = item.IdPublicacion, + NombreParam = item.Nombre, + EstadoParam = item.Estado, + IdUsuarioParam = idUsuarioAuditoria, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminado (Cascada)" }, transaction); } } - - const string deleteSql = "DELETE FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdPublicacion"; + const string deleteSql = "DELETE FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdPublicacionParam"; try { - await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacion = idPublicacion }, transaction); + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacionParam = idPublicacion }, transaction: transaction); return true; } - catch (System.Exception e) + catch (System.Exception ex) { - _log.LogError(e, "Error al eliminar PubliSecciones por IdPublicacion: {idPublicacion}", idPublicacion); + _log.LogError(ex, "Error al eliminar PubliSecciones por IdPublicacion: {idPublicacion}", idPublicacion); throw; } } diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs index ec102ef..7f76a57 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs @@ -1,6 +1,8 @@ +// src/Data/Repositories/PublicacionRepository.cs using Dapper; using GestionIntegral.Api.Models.Distribucion; using Microsoft.Extensions.Logging; +using System; // Añadido para Exception using System.Collections.Generic; using System.Data; using System.Linq; @@ -23,7 +25,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task> GetAllAsync(string? nombreFilter, int? idEmpresaFilter, bool? soloHabilitadas) { var sqlBuilder = new StringBuilder(@" - SELECT p.*, e.Nombre AS NombreEmpresa + SELECT + p.Id_Publicacion AS IdPublicacion, p.Nombre, p.Observacion, p.Id_Empresa AS IdEmpresa, + p.CtrlDevoluciones, p.Habilitada, + e.Nombre AS NombreEmpresa FROM dbo.dist_dtPublicaciones p INNER JOIN dbo.dist_dtEmpresas e ON p.Id_Empresa = e.Id_Empresa WHERE 1=1"); @@ -31,33 +36,34 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion if (soloHabilitadas.HasValue) { - sqlBuilder.Append(soloHabilitadas.Value ? " AND p.Habilitada = 1" : " AND (p.Habilitada = 0 OR p.Habilitada IS NULL)"); + sqlBuilder.Append(soloHabilitadas.Value ? " AND (p.Habilitada = 1 OR p.Habilitada IS NULL)" : " AND p.Habilitada = 0"); } if (!string.IsNullOrWhiteSpace(nombreFilter)) { - sqlBuilder.Append(" AND p.Nombre LIKE @Nombre"); - parameters.Add("Nombre", $"%{nombreFilter}%"); + sqlBuilder.Append(" AND p.Nombre LIKE @NombreParam"); + parameters.Add("NombreParam", $"%{nombreFilter}%"); } if (idEmpresaFilter.HasValue && idEmpresaFilter > 0) { - sqlBuilder.Append(" AND p.Id_Empresa = @IdEmpresa"); - parameters.Add("IdEmpresa", idEmpresaFilter.Value); + sqlBuilder.Append(" AND p.Id_Empresa = @IdEmpresaParam"); + parameters.Add("IdEmpresaParam", idEmpresaFilter.Value); } sqlBuilder.Append(" ORDER BY p.Nombre;"); try { using var connection = _connectionFactory.CreateConnection(); - return await connection.QueryAsync( + var result = await connection.QueryAsync( sqlBuilder.ToString(), - (pub, empNombre) => (pub, empNombre), + (publicacion, nombreEmpresa) => (publicacion, nombreEmpresa), parameters, splitOn: "NombreEmpresa" ); + return result; } catch (Exception ex) { - _logger.LogError(ex, "Error al obtener todas las Publicaciones."); + _logger.LogError(ex, "Error al obtener todas las Publicaciones. Filtros: Nombre='{NombreFilter}', IdEmpresa='{IdEmpresaFilter}', SoloHabilitadas='{SoloHabilitadas}'", nombreFilter, idEmpresaFilter, soloHabilitadas); return Enumerable.Empty<(Publicacion, string)>(); } } @@ -65,17 +71,20 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion public async Task<(Publicacion? Publicacion, string? NombreEmpresa)> GetByIdAsync(int id) { const string sql = @" - SELECT p.*, e.Nombre AS NombreEmpresa + SELECT + p.Id_Publicacion AS IdPublicacion, p.Nombre, p.Observacion, p.Id_Empresa AS IdEmpresa, + p.CtrlDevoluciones, p.Habilitada, + e.Nombre AS NombreEmpresa FROM dbo.dist_dtPublicaciones p INNER JOIN dbo.dist_dtEmpresas e ON p.Id_Empresa = e.Id_Empresa - WHERE p.Id_Publicacion = @Id"; + WHERE p.Id_Publicacion = @IdParam"; try { using var connection = _connectionFactory.CreateConnection(); - var result = await connection.QueryAsync( + var result = await connection.QueryAsync( sql, - (pub, empNombre) => (pub, empNombre), - new { Id = id }, + (publicacion, nombreEmpresa) => (publicacion, nombreEmpresa), + new { IdParam = id }, splitOn: "NombreEmpresa" ); return result.SingleOrDefault(); @@ -86,77 +95,107 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion return (null, null); } } + public async Task GetByIdSimpleAsync(int id) { - const string sql = "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id"; - using var connection = _connectionFactory.CreateConnection(); - return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + const string sql = @" + SELECT + Id_Publicacion AS IdPublicacion, Nombre, Observacion, Id_Empresa AS IdEmpresa, + CtrlDevoluciones, Habilitada + FROM dbo.dist_dtPublicaciones + WHERE Id_Publicacion = @IdParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParam = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Publicación (simple) por ID: {IdPublicacion}", id); + return null; + } } - public async Task ExistsByNameAndEmpresaAsync(string nombre, int idEmpresa, int? excludeIdPublicacion = null) { - var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtPublicaciones WHERE Nombre = @Nombre AND Id_Empresa = @IdEmpresa"); + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtPublicaciones WHERE Nombre = @NombreParam AND Id_Empresa = @IdEmpresaParam"); var parameters = new DynamicParameters(); - parameters.Add("Nombre", nombre); - parameters.Add("IdEmpresa", idEmpresa); + parameters.Add("NombreParam", nombre); + parameters.Add("IdEmpresaParam", idEmpresa); if (excludeIdPublicacion.HasValue) { - sqlBuilder.Append(" AND Id_Publicacion != @ExcludeId"); - parameters.Add("ExcludeId", excludeIdPublicacion.Value); + sqlBuilder.Append(" AND Id_Publicacion != @ExcludeIdParam"); + parameters.Add("ExcludeIdParam", excludeIdPublicacion.Value); + } + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAndEmpresaAsync. Nombre: {Nombre}, IdEmpresa: {IdEmpresa}", nombre, idEmpresa); + return true; // Asumir que existe en caso de error para prevenir duplicados } - using var connection = _connectionFactory.CreateConnection(); - return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); } public async Task IsInUseAsync(int id) { - // Verificar en tablas relacionadas: dist_EntradasSalidas, dist_EntradasSalidasCanillas, - // dist_Precios, dist_RecargoZona, dist_PorcPago, dist_PorcMonPagoCanilla, dist_dtPubliSecciones, - // bob_RegPublicaciones, bob_StockBobinas (donde Id_Publicacion se usa) using var connection = _connectionFactory.CreateConnection(); string[] checkQueries = { - "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidas WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.dist_Precios WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.dist_PorcPago WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.bob_RegPublicaciones WHERE Id_Publicacion = @Id", - "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_Publicacion = @Id" + "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidas WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_Precios WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.bob_RegPublicaciones WHERE Id_Publicacion = @IdParam", + "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_Publicacion = @IdParam" }; - foreach (var query in checkQueries) + try { - if (await connection.ExecuteScalarAsync(query, new { Id = id }) == 1) return true; + foreach (var query in checkQueries) + { + if (await connection.ExecuteScalarAsync(query, new { IdParam = id }) == 1) return true; + } + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Publicacion ID: {IdPublicacion}", id); + return true; // Asumir en uso si hay error de BD } - return false; } - public async Task CreateAsync(Publicacion nuevaPublicacion, int idUsuario, IDbTransaction transaction) { - // Habilitada por defecto es true si es null en el modelo nuevaPublicacion.Habilitada ??= true; const string sqlInsert = @" INSERT INTO dbo.dist_dtPublicaciones (Nombre, Observacion, Id_Empresa, CtrlDevoluciones, Habilitada) - OUTPUT INSERTED.* + OUTPUT INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Nombre, INSERTED.Observacion, INSERTED.Id_Empresa AS IdEmpresa, INSERTED.CtrlDevoluciones, INSERTED.Habilitada VALUES (@Nombre, @Observacion, @IdEmpresa, @CtrlDevoluciones, @Habilitada);"; + + var connection = transaction.Connection!; + var inserted = await connection.QuerySingleAsync(sqlInsert, nuevaPublicacion, transaction); + if (inserted == null || inserted.IdPublicacion == 0) throw new DataException("Error al crear la publicación o al obtener el ID generado."); + const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtPublicaciones_H (Id_Publicacion, Nombre, Observacion, Id_Empresa, Habilitada, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPublicacion, @Nombre, @Observacion, @IdEmpresa, @Habilitada, @Id_Usuario, @FechaMod, @TipoMod);"; - - var connection = transaction.Connection!; - var inserted = await connection.QuerySingleAsync(sqlInsert, nuevaPublicacion, transaction); - if (inserted == null) throw new DataException("Error al crear la publicación."); + VALUES (@IdPublicacionParam, @NombreParam, @ObservacionParam, @IdEmpresaParam, @HabilitadaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - inserted.IdPublicacion, inserted.Nombre, inserted.Observacion, inserted.IdEmpresa, - Habilitada = inserted.Habilitada ?? true, // Asegurar que no sea null para el historial - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creada" + IdPublicacionParam = inserted.IdPublicacion, + NombreParam = inserted.Nombre, + ObservacionParam = inserted.Observacion, + IdEmpresaParam = inserted.IdEmpresa, + HabilitadaParam = inserted.Habilitada ?? true, + IdUsuarioParam = idUsuario, // Renombrado para claridad con el parámetro de la función + FechaModParam = DateTime.Now, + TipoModParam = "Creada" }, transaction); return inserted; } @@ -165,27 +204,33 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { var connection = transaction.Connection!; var actual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @IdPublicacion", - new { publicacionAActualizar.IdPublicacion }, transaction); + @"SELECT Id_Publicacion AS IdPublicacion, Nombre, Observacion, Id_Empresa AS IdEmpresa, CtrlDevoluciones, Habilitada + FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @IdPublicacionParam", // Usar alias + new { IdPublicacionParam = publicacionAActualizar.IdPublicacion }, transaction); if (actual == null) throw new KeyNotFoundException("Publicación no encontrada."); - publicacionAActualizar.Habilitada ??= true; // Asegurar que no sea null + publicacionAActualizar.Habilitada ??= true; const string sqlUpdate = @" UPDATE dbo.dist_dtPublicaciones SET Nombre = @Nombre, Observacion = @Observacion, Id_Empresa = @IdEmpresa, CtrlDevoluciones = @CtrlDevoluciones, Habilitada = @Habilitada - WHERE Id_Publicacion = @IdPublicacion;"; + WHERE Id_Publicacion = @IdPublicacion;"; // Usar nombres de propiedad del objeto const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtPublicaciones_H (Id_Publicacion, Nombre, Observacion, Id_Empresa, Habilitada, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPublicacion, @Nombre, @Observacion, @IdEmpresa, @Habilitada, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdPublicacionParam, @NombreParam, @ObservacionParam, @IdEmpresaParam, @HabilitadaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - IdPublicacion = actual.IdPublicacion, Nombre = actual.Nombre, Observacion = actual.Observacion, - IdEmpresa = actual.IdEmpresa, Habilitada = actual.Habilitada ?? true, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizada" + IdPublicacionParam = actual.IdPublicacion, + NombreParam = actual.Nombre, // Valor ANTES de la actualización + ObservacionParam = actual.Observacion, + IdEmpresaParam = actual.IdEmpresa, + HabilitadaParam = actual.Habilitada ?? true, + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Actualizada" }, transaction); var rowsAffected = await connection.ExecuteAsync(sqlUpdate, publicacionAActualizar, transaction); @@ -196,23 +241,30 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion { var connection = transaction.Connection!; var actual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id", new { Id = id }, transaction); + @"SELECT Id_Publicacion AS IdPublicacion, Nombre, Observacion, Id_Empresa AS IdEmpresa, CtrlDevoluciones, Habilitada + FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @IdParam", // Usar alias + new { IdParam = id }, transaction); if (actual == null) throw new KeyNotFoundException("Publicación no encontrada."); - const string sqlDelete = "DELETE FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id"; + const string sqlDelete = "DELETE FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @IdParam"; const string sqlInsertHistorico = @" INSERT INTO dbo.dist_dtPublicaciones_H (Id_Publicacion, Nombre, Observacion, Id_Empresa, Habilitada, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdPublicacion, @Nombre, @Observacion, @IdEmpresa, @Habilitada, @Id_Usuario, @FechaMod, @TipoMod);"; + VALUES (@IdPublicacionParam, @NombreParam, @ObservacionParam, @IdEmpresaParam, @HabilitadaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; await connection.ExecuteAsync(sqlInsertHistorico, new { - IdPublicacion = actual.IdPublicacion, actual.Nombre, actual.Observacion, actual.IdEmpresa, - Habilitada = actual.Habilitada ?? true, - Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Eliminada" + IdPublicacionParam = actual.IdPublicacion, + NombreParam = actual.Nombre, + ObservacionParam = actual.Observacion, + IdEmpresaParam = actual.IdEmpresa, + HabilitadaParam = actual.Habilitada ?? true, + IdUsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminada" }, transaction); - var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction); + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { IdParam = id }, transaction); return rowsAffected == 1; } } diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs index 1f260f4..a604675 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs @@ -1,16 +1,18 @@ -// src/Data/Repositories/RecargoZonaRepository.cs using Dapper; -using System.Data; -using System.Threading.Tasks; +using GestionIntegral.Api.Models.Distribucion; using Microsoft.Extensions.Logging; -using GestionIntegral.Api.Models.Distribucion; // Asegúrate que esta directiva using esté presente +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; // Necesario para .Any() +using System.Threading.Tasks; namespace GestionIntegral.Api.Data.Repositories.Distribucion { public class RecargoZonaRepository : IRecargoZonaRepository { - private readonly DbConnectionFactory _connectionFactory; // _cf - private readonly ILogger _logger; // _log + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; public RecargoZonaRepository(DbConnectionFactory connectionFactory, ILogger logger) { @@ -18,47 +20,245 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion _logger = logger; } - public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + public async Task> GetByPublicacionIdAsync(int idPublicacion) { - // Obtener recargos para el historial - const string selectRecargos = "SELECT * FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdPublicacion"; - var recargosAEliminar = await transaction.Connection!.QueryAsync(selectRecargos, new { IdPublicacion = idPublicacion }, transaction); - - // Asume que tienes una tabla dist_RecargoZona_H y un modelo RecargoZonaHistorico - const string sqlInsertHistorico = @" - INSERT INTO dbo.dist_RecargoZona_H (Id_Recargo, Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor, Id_Usuario, FechaMod, TipoMod) - VALUES (@IdRecargo, @IdPublicacion, @IdZona, @VigenciaD, @VigenciaH, @Valor, @Id_Usuario, @FechaMod, @TipoMod);"; // Nombres de parámetros corregidos - - foreach (var recargo in recargosAEliminar) - { - await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new - { - // Mapear los campos de 'recargo' a los parámetros de sqlInsertHistorico - recargo.IdRecargo, // Usar las propiedades del modelo RecargoZona - recargo.IdPublicacion, - recargo.IdZona, - recargo.VigenciaD, - recargo.VigenciaH, - recargo.Valor, - Id_Usuario = idUsuarioAuditoria, // Este es el parámetro esperado por la query - FechaMod = DateTime.Now, - TipoMod = "Eliminado (Cascada)" - }, transaction); - } - - const string sql = "DELETE FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdPublicacion"; + const string sql = @" + SELECT + rz.Id_Recargo AS IdRecargo, rz.Id_Publicacion AS IdPublicacion, rz.Id_Zona AS IdZona, + rz.VigenciaD, rz.VigenciaH, rz.Valor, + z.Nombre AS NombreZona + FROM dbo.dist_RecargoZona rz + INNER JOIN dbo.dist_dtZonas z ON rz.Id_Zona = z.Id_Zona + WHERE rz.Id_Publicacion = @IdPublicacionParam + ORDER BY z.Nombre, rz.VigenciaD DESC"; try { - await transaction.Connection!.ExecuteAsync(sql, new { IdPublicacion = idPublicacion }, transaction: transaction); - return true; // Se intentó la operación + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync( + sql, + (recargo, nombreZona) => (recargo, nombreZona), + new { IdPublicacionParam = idPublicacion }, + splitOn: "NombreZona" + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Recargos por Zona para Publicacion ID: {IdPublicacion}", idPublicacion); + return Enumerable.Empty<(RecargoZona, string)>(); + } + } + + public async Task GetByIdAsync(int idRecargo) + { + const string sql = @" + SELECT + Id_Recargo AS IdRecargo, Id_Publicacion AS IdPublicacion, Id_Zona AS IdZona, + VigenciaD, VigenciaH, Valor + FROM dbo.dist_RecargoZona + WHERE Id_Recargo = @IdRecargoParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdRecargoParam = idRecargo }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Recargo por Zona por ID: {IdRecargo}", idRecargo); + return null; + } + } + + public async Task GetActiveByPublicacionZonaAndDateAsync(int idPublicacion, int idZona, DateTime fecha, IDbTransaction? transaction = null) + { + const string sql = @" + SELECT TOP 1 + Id_Recargo AS IdRecargo, Id_Publicacion AS IdPublicacion, Id_Zona AS IdZona, + VigenciaD, VigenciaH, Valor + FROM dbo.dist_RecargoZona + WHERE Id_Publicacion = @IdPublicacionParam AND Id_Zona = @IdZonaParam + AND VigenciaD <= @FechaParam + AND (VigenciaH IS NULL OR VigenciaH >= @FechaParam) + ORDER BY VigenciaD DESC;"; + + var cn = transaction?.Connection ?? _connectionFactory.CreateConnection(); + bool ownConnection = transaction == null; + RecargoZona? result = null; + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) + { + await dbConn.OpenAsync(); + } + result = await cn.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, IdZonaParam = idZona, FechaParam = fecha.Date }, + transaction); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en GetActiveByPublicacionZonaAndDateAsync. IdPublicacion: {IdPublicacion}, IdZona: {IdZona}, Fecha: {Fecha}", idPublicacion, idZona, fecha); + throw; + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) + { + await dbConnClose.CloseAsync(); + } + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; + } + + public async Task GetPreviousActiveRecargoAsync(int idPublicacion, int idZona, DateTime vigenciaDNuevo, IDbTransaction transaction) + { + const string sql = @" + SELECT TOP 1 + Id_Recargo AS IdRecargo, Id_Publicacion AS IdPublicacion, Id_Zona AS IdZona, + VigenciaD, VigenciaH, Valor + FROM dbo.dist_RecargoZona + WHERE Id_Publicacion = @IdPublicacionParam AND Id_Zona = @IdZonaParam + AND VigenciaD < @VigenciaDNuevoParam + AND VigenciaH IS NULL + ORDER BY VigenciaD DESC;"; + return await transaction.Connection!.QuerySingleOrDefaultAsync(sql, + new { IdPublicacionParam = idPublicacion, IdZonaParam = idZona, VigenciaDNuevoParam = vigenciaDNuevo.Date }, + transaction); + } + + public async Task CreateAsync(RecargoZona nuevoRecargo, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_RecargoZona (Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor) + OUTPUT INSERTED.Id_Recargo AS IdRecargo, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Zona AS IdZona, + INSERTED.VigenciaD, INSERTED.VigenciaH, INSERTED.Valor + VALUES (@IdPublicacion, @IdZona, @VigenciaD, @VigenciaH, @Valor);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_RecargoZona_H (Id_Recargo, Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRecargoParam, @IdPublicacionParam, @IdZonaParam, @VigenciaDParam, @VigenciaHParam, @ValorParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoRecargo, transaction); + if (inserted == null || inserted.IdRecargo == 0) throw new DataException("Error al crear recargo por zona o al obtener el ID generado."); + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdRecargoParam = inserted.IdRecargo, + IdPublicacionParam = inserted.IdPublicacion, + IdZonaParam = inserted.IdZona, + VigenciaDParam = inserted.VigenciaD, + VigenciaHParam = inserted.VigenciaH, + ValorParam = inserted.Valor, + Id_UsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(RecargoZona recargoAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Recargo AS IdRecargo, Id_Publicacion AS IdPublicacion, Id_Zona AS IdZona, + VigenciaD, VigenciaH, Valor + FROM dbo.dist_RecargoZona WHERE Id_Recargo = @IdRecargoParam", // Usar parámetro con alias + new { IdRecargoParam = recargoAActualizar.IdRecargo }, transaction); + if (actual == null) throw new KeyNotFoundException("Recargo por zona no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_RecargoZona SET + Valor = @Valor, VigenciaH = @VigenciaH + WHERE Id_Recargo = @IdRecargo;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_RecargoZona_H (Id_Recargo, Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRecargoParam, @IdPublicacionParam, @IdZonaParam, @VigenciaDParam, @VigenciaHParam, @ValorParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdRecargoParam = actual.IdRecargo, + IdPublicacionParam = actual.IdPublicacion, + IdZonaParam = actual.IdZona, + VigenciaDParam = actual.VigenciaD, + VigenciaHParam = actual.VigenciaH, + ValorParam = actual.Valor, + Id_UsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Actualizado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, recargoAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idRecargo, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Recargo AS IdRecargo, Id_Publicacion AS IdPublicacion, Id_Zona AS IdZona, + VigenciaD, VigenciaH, Valor + FROM dbo.dist_RecargoZona WHERE Id_Recargo = @IdRecargoParam", new { IdRecargoParam = idRecargo }, transaction); + if (actual == null) throw new KeyNotFoundException("Recargo por zona no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_RecargoZona WHERE Id_Recargo = @IdRecargoParam"; // Usar parámetro con alias + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_RecargoZona_H (Id_Recargo, Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRecargoParam, @IdPublicacionParam, @IdZonaParam, @VigenciaDParam, @VigenciaHParam, @ValorParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdRecargoParam = actual.IdRecargo, + IdPublicacionParam = actual.IdPublicacion, + IdZonaParam = actual.IdZona, + VigenciaDParam = actual.VigenciaD, + VigenciaHParam = actual.VigenciaH, + ValorParam = actual.Valor, + Id_UsuarioParam = idUsuario, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdRecargoParam = idRecargo }, transaction); + return rowsAffected == 1; + } + + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + { + const string selectSql = @" + SELECT + Id_Recargo AS IdRecargo, Id_Publicacion AS IdPublicacion, Id_Zona AS IdZona, + VigenciaD, VigenciaH, Valor + FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdPublicacionParam"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacionParam = idPublicacion }, transaction); + + if (itemsToDelete.Any()) + { + const string insertHistoricoSql = @" + INSERT INTO dbo.dist_RecargoZona_H (Id_Recargo, Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRecargoParam, @IdPublicacionParam, @IdZonaParam, @VigenciaDParam, @VigenciaHParam, @ValorParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);"; + foreach (var item in itemsToDelete) + { + await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new + { + IdRecargoParam = item.IdRecargo, + IdPublicacionParam = item.IdPublicacion, + IdZonaParam = item.IdZona, + VigenciaDParam = item.VigenciaD, + VigenciaHParam = item.VigenciaH, + ValorParam = item.Valor, + Id_UsuarioParam = idUsuarioAuditoria, + FechaModParam = DateTime.Now, + TipoModParam = "Eliminado (Cascada)" + }, transaction); + } + } + const string deleteSql = "DELETE FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdPublicacionParam"; + try + { + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacionParam = idPublicacion }, transaction: transaction); + return true; } catch (System.Exception ex) { _logger.LogError(ex, "Error al eliminar RecargosZona por IdPublicacion: {IdPublicacion}", idPublicacion); - throw; // Re-lanzar para que la transacción padre haga rollback + throw; } } - // Aquí irían otros métodos CRUD para RecargoZona si se gestionan individualmente. - // Por ejemplo, GetByPublicacionId, Create, Update, Delete (para un recargo específico). } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/SalidaOtroDestinoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/SalidaOtroDestinoRepository.cs new file mode 100644 index 0000000..7a4f9b4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/SalidaOtroDestinoRepository.cs @@ -0,0 +1,144 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class SalidaOtroDestinoRepository : ISalidaOtroDestinoRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public SalidaOtroDestinoRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + public async Task> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino) + { + var sqlBuilder = new StringBuilder(@" + SELECT + Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Destino AS IdDestino, + Fecha, Cantidad, Observacion + FROM dbo.dist_SalidasOtrosDestinos + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) { sqlBuilder.Append(" AND Fecha >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } + if (fechaHasta.HasValue) { sqlBuilder.Append(" AND Fecha <= @FechaHastaParam"); parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } + if (idPublicacion.HasValue) { sqlBuilder.Append(" AND Id_Publicacion = @IdPublicacionParam"); parameters.Add("IdPublicacionParam", idPublicacion.Value); } + if (idDestino.HasValue) { sqlBuilder.Append(" AND Id_Destino = @IdDestinoParam"); parameters.Add("IdDestinoParam", idDestino.Value); } + sqlBuilder.Append(" ORDER BY Fecha DESC, Id_Publicacion;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Salidas a Otros Destinos."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idParte) + { + const string sql = @" + SELECT + Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Destino AS IdDestino, + Fecha, Cantidad, Observacion + FROM dbo.dist_SalidasOtrosDestinos + WHERE Id_Parte = @IdParteParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParteParam = idParte }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener SalidaOtroDestino por ID: {IdParte}", idParte); + return null; + } + } + + public async Task CreateAsync(SalidaOtroDestino nuevaSalida, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_SalidasOtrosDestinos (Id_Publicacion, Id_Destino, Fecha, Cantidad, Observacion) + OUTPUT INSERTED.Id_Parte AS IdParte, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Destino AS IdDestino, + INSERTED.Fecha, INSERTED.Cantidad, INSERTED.Observacion + VALUES (@IdPublicacion, @IdDestino, @Fecha, @Cantidad, @Observacion);"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_SalidasOtrosDestinos_H (Id_Parte, Id_Publicacion, Id_Destino, Fecha, Cantidad, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPublicacionHist, @IdDestinoHist, @FechaHist, @CantidadHist, @ObservacionHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaSalida, transaction); + if (inserted == null || inserted.IdParte == 0) throw new DataException("Error al crear salida o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdParteHist = inserted.IdParte, IdPublicacionHist = inserted.IdPublicacion, IdDestinoHist = inserted.IdDestino, + FechaHist = inserted.Fecha, CantidadHist = inserted.Cantidad, ObservacionHist = inserted.Observacion, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Creada" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(SalidaOtroDestino salidaAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Destino AS IdDestino, Fecha, Cantidad, Observacion + FROM dbo.dist_SalidasOtrosDestinos WHERE Id_Parte = @IdParteParam", + new { IdParteParam = salidaAActualizar.IdParte }, transaction); + if (actual == null) throw new KeyNotFoundException("Salida no encontrada."); + + const string sqlUpdate = @" + UPDATE dbo.dist_SalidasOtrosDestinos SET + Cantidad = @Cantidad, Observacion = @Observacion + -- No se permite cambiar Publicacion, Destino o Fecha de una salida ya registrada + WHERE Id_Parte = @IdParte;"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_SalidasOtrosDestinos_H (Id_Parte, Id_Publicacion, Id_Destino, Fecha, Cantidad, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPublicacionHist, @IdDestinoHist, @FechaHist, @CantidadHist, @ObservacionHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdParteHist = actual.IdParte, IdPublicacionHist = actual.IdPublicacion, IdDestinoHist = actual.IdDestino, + FechaHist = actual.Fecha, CantidadHist = actual.Cantidad, ObservacionHist = actual.Observacion, // Valores ANTERIORES + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Actualizada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, salidaAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Destino AS IdDestino, Fecha, Cantidad, Observacion + FROM dbo.dist_SalidasOtrosDestinos WHERE Id_Parte = @IdParteParam", + new { IdParteParam = idParte }, transaction); + if (actual == null) throw new KeyNotFoundException("Salida no encontrada para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_SalidasOtrosDestinos WHERE Id_Parte = @IdParteParam"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_SalidasOtrosDestinos_H (Id_Parte, Id_Publicacion, Id_Destino, Fecha, Cantidad, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPublicacionHist, @IdDestinoHist, @FechaHist, @CantidadHist, @ObservacionHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdParteHist = actual.IdParte, IdPublicacionHist = actual.IdPublicacion, IdDestinoHist = actual.IdDestino, + FechaHist = actual.Fecha, CantidadHist = actual.Cantidad, ObservacionHist = actual.Observacion, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Eliminada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdParteParam = idParte }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IRegTiradaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IRegTiradaRepository.cs new file mode 100644 index 0000000..c8bc0a7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IRegTiradaRepository.cs @@ -0,0 +1,27 @@ +using GestionIntegral.Api.Models.Impresion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public interface IRegTiradaRepository // Para bob_RegTiradas + { + Task GetByIdAsync(int idRegistro); + Task> GetByCriteriaAsync(DateTime? fecha, int? idPublicacion, int? idPlanta); + Task CreateAsync(RegTirada nuevaTirada, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction); // Si se borra el registro principal + Task DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); + Task GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, IDbTransaction? transaction = null); + + } + + public interface IRegPublicacionSeccionRepository // Para bob_RegPublicaciones + { + Task> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta); + Task CreateAsync(RegPublicacionSeccion nuevaSeccionTirada, int idUsuario, IDbTransaction transaction); + Task DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction); + // Podría tener un DeleteByIdAsync si se permite borrar secciones individuales de una tirada + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IStockBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IStockBobinaRepository.cs new file mode 100644 index 0000000..03fd767 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IStockBobinaRepository.cs @@ -0,0 +1,26 @@ +using GestionIntegral.Api.Models.Impresion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IStockBobinaRepository + { + Task> GetAllAsync( // Para listados generales, DTO se arma en servicio + int? idTipoBobina, + string? nroBobinaFilter, + int? idPlanta, + int? idEstadoBobina, + string? remitoFilter, + DateTime? fechaDesde, + DateTime? fechaHasta); + + Task GetByIdAsync(int idBobina); + Task GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina + Task CreateAsync(StockBobina nuevaBobina, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(StockBobina bobinaAActualizar, int idUsuario, IDbTransaction transaction, string tipoMod = "Actualizada"); // tipoMod para historial + Task DeleteAsync(int idBobina, int idUsuario, IDbTransaction transaction); // Solo si está en estado "Disponible" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/RegTiradaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/RegTiradaRepository.cs new file mode 100644 index 0000000..c8e5166 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/RegTiradaRepository.cs @@ -0,0 +1,168 @@ +using Dapper; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public class RegTiradaRepository : IRegTiradaRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public RegTiradaRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + public async Task GetByIdAsync(int idRegistro) + { + const string sql = "SELECT Id_Registro AS IdRegistro, Ejemplares, Id_Publicacion AS IdPublicacion, Fecha, Id_Planta AS IdPlanta FROM dbo.bob_RegTiradas WHERE Id_Registro = @IdRegistroParam"; + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdRegistroParam = idRegistro }); + } + public async Task GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, IDbTransaction? transaction = null) + { + const string sql = "SELECT Id_Registro AS IdRegistro, Ejemplares, Id_Publicacion AS IdPublicacion, Fecha, Id_Planta AS IdPlanta FROM dbo.bob_RegTiradas WHERE Fecha = @FechaParam AND Id_Publicacion = @IdPublicacionParam AND Id_Planta = @IdPlantaParam"; + var cn = transaction?.Connection ?? _cf.CreateConnection(); + bool ownConnection = transaction == null; + RegTirada? result = null; + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); + result = await cn.QuerySingleOrDefaultAsync(sql, new { FechaParam = fecha.Date, IdPublicacionParam = idPublicacion, IdPlantaParam = idPlanta }, transaction); + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; + } + + + public async Task> GetByCriteriaAsync(DateTime? fecha, int? idPublicacion, int? idPlanta) + { + var sqlBuilder = new StringBuilder("SELECT Id_Registro AS IdRegistro, Ejemplares, Id_Publicacion AS IdPublicacion, Fecha, Id_Planta AS IdPlanta FROM dbo.bob_RegTiradas WHERE 1=1 "); + var parameters = new DynamicParameters(); + if (fecha.HasValue) { sqlBuilder.Append("AND Fecha = @FechaParam "); parameters.Add("FechaParam", fecha.Value.Date); } + if (idPublicacion.HasValue) { sqlBuilder.Append("AND Id_Publicacion = @IdPublicacionParam "); parameters.Add("IdPublicacionParam", idPublicacion.Value); } + if (idPlanta.HasValue) { sqlBuilder.Append("AND Id_Planta = @IdPlantaParam "); parameters.Add("IdPlantaParam", idPlanta.Value); } + sqlBuilder.Append("ORDER BY Fecha DESC, Id_Publicacion;"); + + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + + public async Task CreateAsync(RegTirada nuevaTirada, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.bob_RegTiradas (Ejemplares, Id_Publicacion, Fecha, Id_Planta) + OUTPUT INSERTED.Id_Registro AS IdRegistro, INSERTED.Ejemplares, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Fecha, INSERTED.Id_Planta AS IdPlanta + VALUES (@Ejemplares, @IdPublicacion, @Fecha, @IdPlanta);"; + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaTirada, transaction); + + const string sqlHistorico = @"INSERT INTO dbo.bob_RegTiradas_H (Id_Registro, Ejemplares, Id_Publicacion, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRegistroParam, @EjemplaresParam, @IdPublicacionParam, @FechaParam, @IdPlantaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdRegistroParam = inserted.IdRegistro, EjemplaresParam = inserted.Ejemplares, IdPublicacionParam = inserted.IdPublicacion, FechaParam = inserted.Fecha, IdPlantaParam = inserted.IdPlanta, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Creado" + }, transaction); + return inserted; + } + + public async Task DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction) + { + var actual = await GetByIdAsync(idRegistro); // No necesita TX aquí ya que es solo para historial + if (actual == null) throw new KeyNotFoundException("Registro de tirada no encontrado."); + + const string sqlDelete = "DELETE FROM dbo.bob_RegTiradas WHERE Id_Registro = @IdRegistroParam"; + const string sqlHistorico = @"INSERT INTO dbo.bob_RegTiradas_H (Id_Registro, Ejemplares, Id_Publicacion, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRegistroParam, @EjemplaresParam, @IdPublicacionParam, @FechaParam, @IdPlantaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdRegistroParam = actual.IdRegistro, EjemplaresParam = actual.Ejemplares, IdPublicacionParam = actual.IdPublicacion, FechaParam = actual.Fecha, IdPlantaParam = actual.IdPlanta, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdRegistroParam = idRegistro }, transaction); + return rowsAffected == 1; + } + public async Task DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction) + { + var tiradasAEliminar = await GetByCriteriaAsync(fecha.Date, idPublicacion, idPlanta); // Obtener antes de borrar para historial + + foreach(var actual in tiradasAEliminar) + { + const string sqlHistorico = @"INSERT INTO dbo.bob_RegTiradas_H (Id_Registro, Ejemplares, Id_Publicacion, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRegistroParam, @EjemplaresParam, @IdPublicacionParam, @FechaParam, @IdPlantaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdRegistroParam = actual.IdRegistro, EjemplaresParam = actual.Ejemplares, IdPublicacionParam = actual.IdPublicacion, FechaParam = actual.Fecha, IdPlantaParam = actual.IdPlanta, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Eliminado (Por Fecha/Pub/Planta)" + }, transaction); + } + + const string sqlDelete = "DELETE FROM dbo.bob_RegTiradas WHERE Fecha = @FechaParam AND Id_Publicacion = @IdPublicacionParam AND Id_Planta = @IdPlantaParam"; + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, + new { FechaParam = fecha.Date, IdPublicacionParam = idPublicacion, IdPlantaParam = idPlanta }, transaction); + return rowsAffected > 0; // Devuelve true si se borró al menos una fila + } + } + + public class RegPublicacionSeccionRepository : IRegPublicacionSeccionRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + public RegPublicacionSeccionRepository(DbConnectionFactory cf, ILogger log){ _cf = cf; _log = log; } + + public async Task> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta) + { + const string sql = @"SELECT Id_Tirada AS IdTirada, Id_Publicacion AS IdPublicacion, Id_Seccion AS IdSeccion, CantPag, Fecha, Id_Planta AS IdPlanta + FROM dbo.bob_RegPublicaciones + WHERE Fecha = @FechaParam AND Id_Publicacion = @IdPublicacionParam AND Id_Planta = @IdPlantaParam"; + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sql, new { FechaParam = fecha.Date, IdPublicacionParam = idPublicacion, IdPlantaParam = idPlanta }); + } + + public async Task CreateAsync(RegPublicacionSeccion nuevaSeccionTirada, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.bob_RegPublicaciones (Id_Publicacion, Id_Seccion, CantPag, Fecha, Id_Planta) + OUTPUT INSERTED.Id_Tirada AS IdTirada, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Seccion AS IdSeccion, INSERTED.CantPag, INSERTED.Fecha, INSERTED.Id_Planta AS IdPlanta + VALUES (@IdPublicacion, @IdSeccion, @CantPag, @Fecha, @IdPlanta);"; + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaSeccionTirada, transaction); + + const string sqlHistorico = @"INSERT INTO dbo.bob_RegPublicaciones_H (Id_Tirada, Id_Publicacion, Id_Seccion, CantPag, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTiradaParam, @IdPublicacionParam, @IdSeccionParam, @CantPagParam, @FechaParam, @IdPlantaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdTiradaParam = inserted.IdTirada, IdPublicacionParam = inserted.IdPublicacion, IdSeccionParam = inserted.IdSeccion, CantPagParam = inserted.CantPag, FechaParam = inserted.Fecha, IdPlantaParam = inserted.IdPlanta, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Creado" + }, transaction); + return inserted; + } + + public async Task DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction) + { + var seccionesAEliminar = await GetByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta); // No necesita TX, es SELECT + + foreach(var actual in seccionesAEliminar) + { + const string sqlHistorico = @"INSERT INTO dbo.bob_RegPublicaciones_H (Id_Tirada, Id_Publicacion, Id_Seccion, CantPag, Fecha, Id_Planta, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTiradaParam, @IdPublicacionParam, @IdSeccionParam, @CantPagParam, @FechaParam, @IdPlantaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdTiradaParam = actual.IdTirada, IdPublicacionParam = actual.IdPublicacion, IdSeccionParam = actual.IdSeccion, CantPagParam = actual.CantPag, FechaParam = actual.Fecha, IdPlantaParam = actual.IdPlanta, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Eliminado (Por Fecha/Pub/Planta)" + }, transaction); + } + + const string sqlDelete = "DELETE FROM dbo.bob_RegPublicaciones WHERE Fecha = @FechaParam AND Id_Publicacion = @IdPublicacionParam AND Id_Planta = @IdPlantaParam"; + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, + new { FechaParam = fecha.Date, IdPublicacionParam = idPublicacion, IdPlantaParam = idPlanta }, transaction); + return rowsAffected >= 0; // Devuelve true incluso si no había nada que borrar (para que no falle la TX si es parte de una cadena) + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs new file mode 100644 index 0000000..51b3ab9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/StockBobinaRepository.cs @@ -0,0 +1,232 @@ +using Dapper; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class StockBobinaRepository : IStockBobinaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public StockBobinaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync( + int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, + int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta) + { + var sqlBuilder = new StringBuilder(@" + SELECT + sb.Id_Bobina AS IdBobina, sb.Id_TipoBobina AS IdTipoBobina, sb.NroBobina, sb.Peso, + sb.Id_Planta AS IdPlanta, sb.Id_EstadoBobina AS IdEstadoBobina, sb.Remito, sb.FechaRemito, + sb.FechaEstado, sb.Id_Publicacion AS IdPublicacion, sb.Id_Seccion AS IdSeccion, sb.Obs + FROM dbo.bob_StockBobinas sb + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (idTipoBobina.HasValue) + { + sqlBuilder.Append(" AND sb.Id_TipoBobina = @IdTipoBobinaParam"); + parameters.Add("IdTipoBobinaParam", idTipoBobina.Value); + } + if (!string.IsNullOrWhiteSpace(nroBobinaFilter)) + { + sqlBuilder.Append(" AND sb.NroBobina LIKE @NroBobinaParam"); + parameters.Add("NroBobinaParam", $"%{nroBobinaFilter}%"); + } + if (idPlanta.HasValue) + { + sqlBuilder.Append(" AND sb.Id_Planta = @IdPlantaParam"); + parameters.Add("IdPlantaParam", idPlanta.Value); + } + if (idEstadoBobina.HasValue) + { + sqlBuilder.Append(" AND sb.Id_EstadoBobina = @IdEstadoBobinaParam"); + parameters.Add("IdEstadoBobinaParam", idEstadoBobina.Value); + } + if (!string.IsNullOrWhiteSpace(remitoFilter)) + { + sqlBuilder.Append(" AND sb.Remito LIKE @RemitoParam"); + parameters.Add("RemitoParam", $"%{remitoFilter}%"); + } + if (fechaDesde.HasValue) + { + sqlBuilder.Append(" AND sb.FechaRemito >= @FechaDesdeParam"); // O FechaEstado según el contexto del filtro + parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); + } + if (fechaHasta.HasValue) + { + sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); // O FechaEstado + parameters.Add("FechaHastaParam", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); // Hasta el final del día + } + + sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Stock de Bobinas con filtros."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idBobina) + { + const string sql = @" + SELECT + Id_Bobina AS IdBobina, Id_TipoBobina AS IdTipoBobina, NroBobina, Peso, + Id_Planta AS IdPlanta, Id_EstadoBobina AS IdEstadoBobina, Remito, FechaRemito, + FechaEstado, Id_Publicacion AS IdPublicacion, Id_Seccion AS IdSeccion, Obs + FROM dbo.bob_StockBobinas + WHERE Id_Bobina = @IdBobinaParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdBobinaParam = idBobina }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener StockBobina por ID: {IdBobina}", idBobina); + return null; + } + } + + public async Task GetByNroBobinaAsync(string nroBobina) + { + const string sql = @" + SELECT + Id_Bobina AS IdBobina, Id_TipoBobina AS IdTipoBobina, NroBobina, Peso, + Id_Planta AS IdPlanta, Id_EstadoBobina AS IdEstadoBobina, Remito, FechaRemito, + FechaEstado, Id_Publicacion AS IdPublicacion, Id_Seccion AS IdSeccion, Obs + FROM dbo.bob_StockBobinas + WHERE NroBobina = @NroBobinaParam"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { NroBobinaParam = nroBobina }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener StockBobina por NroBobina: {NroBobina}", nroBobina); + return null; + } + } + + + public async Task CreateAsync(StockBobina nuevaBobina, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.bob_StockBobinas + (Id_TipoBobina, NroBobina, Peso, Id_Planta, Id_EstadoBobina, Remito, FechaRemito, FechaEstado, Id_Publicacion, Id_Seccion, Obs) + OUTPUT INSERTED.Id_Bobina AS IdBobina, INSERTED.Id_TipoBobina AS IdTipoBobina, INSERTED.NroBobina, INSERTED.Peso, + INSERTED.Id_Planta AS IdPlanta, INSERTED.Id_EstadoBobina AS IdEstadoBobina, INSERTED.Remito, INSERTED.FechaRemito, + INSERTED.FechaEstado, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Seccion AS IdSeccion, INSERTED.Obs + VALUES (@IdTipoBobina, @NroBobina, @Peso, @IdPlanta, @IdEstadoBobina, @Remito, @FechaRemito, @FechaEstado, @IdPublicacion, @IdSeccion, @Obs);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaBobina, transaction); + if (inserted == null || inserted.IdBobina == 0) throw new DataException("Error al ingresar bobina o ID no generado."); + + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_StockBobinas_H + (Id_Bobina, Id_TipoBobina, NroBobina, Peso, Id_Planta, Id_EstadoBobina, Remito, FechaRemito, FechaEstado, Id_Publicacion, Id_Seccion, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdBobinaHist, @IdTipoBobinaHist, @NroBobinaHist, @PesoHist, @IdPlantaHist, @IdEstadoBobinaHist, @RemitoHist, @FechaRemitoHist, @FechaEstadoHist, @IdPublicacionHist, @IdSeccionHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdBobinaHist = inserted.IdBobina, IdTipoBobinaHist = inserted.IdTipoBobina, NroBobinaHist = inserted.NroBobina, PesoHist = inserted.Peso, + IdPlantaHist = inserted.IdPlanta, IdEstadoBobinaHist = inserted.IdEstadoBobina, RemitoHist = inserted.Remito, FechaRemitoHist = inserted.FechaRemito, + FechaEstadoHist = inserted.FechaEstado, IdPublicacionHist = inserted.IdPublicacion, IdSeccionHist = inserted.IdSeccion, ObsHist = inserted.Obs, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Ingreso" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(StockBobina bobinaAActualizar, int idUsuario, IDbTransaction transaction, string tipoMod = "Actualizada") + { + // Obtener estado actual para el historial + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Bobina AS IdBobina, Id_TipoBobina AS IdTipoBobina, NroBobina, Peso, + Id_Planta AS IdPlanta, Id_EstadoBobina AS IdEstadoBobina, Remito, FechaRemito, + FechaEstado, Id_Publicacion AS IdPublicacion, Id_Seccion AS IdSeccion, Obs + FROM dbo.bob_StockBobinas WHERE Id_Bobina = @IdBobinaParam", + new { IdBobinaParam = bobinaAActualizar.IdBobina }, transaction); + if (actual == null) throw new KeyNotFoundException("Bobina no encontrada para actualizar."); + + const string sqlUpdate = @" + UPDATE dbo.bob_StockBobinas SET + Id_TipoBobina = @IdTipoBobina, NroBobina = @NroBobina, Peso = @Peso, Id_Planta = @IdPlanta, + Id_EstadoBobina = @IdEstadoBobina, Remito = @Remito, FechaRemito = @FechaRemito, FechaEstado = @FechaEstado, + Id_Publicacion = @IdPublicacion, Id_Seccion = @IdSeccion, Obs = @Obs + WHERE Id_Bobina = @IdBobina;"; + + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_StockBobinas_H + (Id_Bobina, Id_TipoBobina, NroBobina, Peso, Id_Planta, Id_EstadoBobina, Remito, FechaRemito, FechaEstado, Id_Publicacion, Id_Seccion, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdBobinaHist, @IdTipoBobinaHist, @NroBobinaHist, @PesoHist, @IdPlantaHist, @IdEstadoBobinaHist, @RemitoHist, @FechaRemitoHist, @FechaEstadoHist, @IdPublicacionHist, @IdSeccionHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdBobinaHist = actual.IdBobina, IdTipoBobinaHist = actual.IdTipoBobina, NroBobinaHist = actual.NroBobina, PesoHist = actual.Peso, + IdPlantaHist = actual.IdPlanta, IdEstadoBobinaHist = actual.IdEstadoBobina, RemitoHist = actual.Remito, FechaRemitoHist = actual.FechaRemito, + FechaEstadoHist = actual.FechaEstado, IdPublicacionHist = actual.IdPublicacion, IdSeccionHist = actual.IdSeccion, ObsHist = actual.Obs, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = tipoMod // "Actualizada" o "Estado Cambiado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, bobinaAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idBobina, int idUsuario, IDbTransaction transaction) + { + // Normalmente, una bobina en stock no se "elimina" físicamente si ya tuvo movimientos. + // Este método sería para borrar un ingreso erróneo que aún esté "Disponible". + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + @"SELECT Id_Bobina AS IdBobina, Id_TipoBobina AS IdTipoBobina, NroBobina, Peso, + Id_Planta AS IdPlanta, Id_EstadoBobina AS IdEstadoBobina, Remito, FechaRemito, + FechaEstado, Id_Publicacion AS IdPublicacion, Id_Seccion AS IdSeccion, Obs + FROM dbo.bob_StockBobinas WHERE Id_Bobina = @IdBobinaParam", + new { IdBobinaParam = idBobina }, transaction); + if (actual == null) throw new KeyNotFoundException("Bobina no encontrada para eliminar."); + + // VALIDACIÓN IMPORTANTE: Solo permitir borrar si está en estado "Disponible" (o el que se defina) + if (actual.IdEstadoBobina != 1) // Asumiendo 1 = Disponible + { + _logger.LogWarning("Intento de eliminar bobina {IdBobina} que no está en estado 'Disponible'. Estado actual: {EstadoActual}", idBobina, actual.IdEstadoBobina); + // No lanzar excepción para que la transacción no falle si es una validación de negocio + return false; + } + + const string sqlDelete = "DELETE FROM dbo.bob_StockBobinas WHERE Id_Bobina = @IdBobinaParam"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_StockBobinas_H + (Id_Bobina, Id_TipoBobina, NroBobina, Peso, Id_Planta, Id_EstadoBobina, Remito, FechaRemito, FechaEstado, Id_Publicacion, Id_Seccion, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdBobinaHist, @IdTipoBobinaHist, @NroBobinaHist, @PesoHist, @IdPlantaHist, @IdEstadoBobinaHist, @RemitoHist, @FechaRemitoHist, @FechaEstadoHist, @IdPublicacionHist, @IdSeccionHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + IdBobinaHist = actual.IdBobina, IdTipoBobinaHist = actual.IdTipoBobina, NroBobinaHist = actual.NroBobina, PesoHist = actual.Peso, + IdPlantaHist = actual.IdPlanta, IdEstadoBobinaHist = actual.IdEstadoBobina, RemitoHist = actual.Remito, FechaRemitoHist = actual.FechaRemito, + FechaEstadoHist = actual.FechaEstado, IdPublicacionHist = actual.IdPublicacion, IdSeccionHist = actual.IdSeccion, ObsHist = actual.Obs, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Eliminada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdBobinaParam = idBobina }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaDist.cs b/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaDist.cs new file mode 100644 index 0000000..8bb8f66 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaDist.cs @@ -0,0 +1,18 @@ +using System; +namespace GestionIntegral.Api.Models.Distribucion +{ + public class EntradaSalidaDist // Corresponde a dist_EntradasSalidas + { + public int IdParte { get; set; } + public int IdPublicacion { get; set; } + public int IdDistribuidor { get; set; } + public DateTime Fecha { get; set; } + public string TipoMovimiento { get; set; } = string.Empty; // "Salida" o "Entrada" + public int Cantidad { get; set; } + public int Remito { get; set; } // Número de remito + public string? Observacion { get; set; } + public int IdPrecio { get; set; } // FK a dist_Precios + public int IdRecargo { get; set; } // FK a dist_RecargoZona (puede ser 0 si no aplica) + public int IdPorcentaje { get; set; } // FK a dist_PorcPago (puede ser 0 si no aplica) + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaDistHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaDistHistorico.cs new file mode 100644 index 0000000..0a1ab68 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaDistHistorico.cs @@ -0,0 +1,21 @@ +using System; +namespace GestionIntegral.Api.Models.Distribucion +{ + public class EntradaSalidaDistHistorico // Corresponde a dist_EntradasSalidas_H + { + public int Id_Parte { get; set; } // Coincide con columna en _H + public int Id_Publicacion { get; set; } + public int Id_Distribuidor { get; set; } + public DateTime Fecha { get; set; } + public string TipoMovimiento { get; set; } = string.Empty; + public int Cantidad { get; set; } + public int Remito { get; set; } + public string? Observacion { get; set; } + public int Id_Precio { get; set; } + public int Id_Recargo { get; set; } + public int Id_Porcentaje { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/SalidaOtroDestino.cs b/Backend/GestionIntegral.Api/Models/Distribucion/SalidaOtroDestino.cs new file mode 100644 index 0000000..830cb2a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/SalidaOtroDestino.cs @@ -0,0 +1,14 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class SalidaOtroDestino + { + public int IdParte { get; set; } // Id_Parte (PK, Identity) + public int IdPublicacion { get; set; } + public int IdDestino { get; set; } // FK a dist_dtOtrosDestinos + public DateTime Fecha { get; set; } + public int Cantidad { get; set; } + public string? Observacion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/SalidaOtroDestinoHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/SalidaOtroDestinoHistorico.cs new file mode 100644 index 0000000..f4a4503 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/SalidaOtroDestinoHistorico.cs @@ -0,0 +1,20 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class SalidaOtroDestinoHistorico // Corresponde a dist_SalidasOtrosDestinos_H + { + // No hay PK autoincremental explícita en el script de _H + public int Id_Parte { get; set; } // Coincide con columna en _H + public int Id_Publicacion { get; set; } + public int Id_Destino { get; set; } + public DateTime Fecha { get; set; } + public int Cantidad { get; set; } + public string? Observacion { get; set; } + + // Auditoría + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Creada", "Actualizada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaDistDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaDistDto.cs new file mode 100644 index 0000000..8aa1478 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaDistDto.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateEntradaSalidaDistDto + { + [Required] + public int IdPublicacion { get; set; } + [Required] + public int IdDistribuidor { get; set; } + [Required] + public DateTime Fecha { get; set; } + [Required] + [RegularExpression("^(Salida|Entrada)$", ErrorMessage = "Tipo de movimiento debe ser 'Salida' o 'Entrada'.")] + public string TipoMovimiento { get; set; } = string.Empty; + [Required, Range(1, int.MaxValue)] + public int Cantidad { get; set; } + [Required, Range(1, int.MaxValue)] // Asumiendo que el remito es un número > 0 + public int Remito { get; set; } + [StringLength(150)] + public string? Observacion { get; set; } + // IdPrecio, IdRecargo, IdPorcentaje se determinarán en el backend. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePorcMonCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePorcMonCanillaDto.cs new file mode 100644 index 0000000..27dcf5e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePorcMonCanillaDto.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreatePorcMonCanillaDto + { + [Required] + public int IdPublicacion { get; set; } + + [Required(ErrorMessage = "El canillita es obligatorio.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar un canillita válido.")] + public int IdCanilla { get; set; } + + [Required(ErrorMessage = "La fecha de Vigencia Desde es obligatoria.")] + public DateTime VigenciaD { get; set; } + + [Required(ErrorMessage = "El valor (porcentaje o monto) es obligatorio.")] + [Range(0, double.MaxValue, ErrorMessage = "El valor debe ser un monto positivo.")] // double.MaxValue para permitir porcentajes > 100 si fuera necesario, ajustar + public decimal PorcMon { get; set; } + + [Required] + public bool EsPorcentaje { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePorcPagoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePorcPagoDto.cs new file mode 100644 index 0000000..6612255 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePorcPagoDto.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreatePorcPagoDto + { + [Required] + public int IdPublicacion { get; set; } + + [Required(ErrorMessage = "El distribuidor es obligatorio.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar un distribuidor válido.")] + public int IdDistribuidor { get; set; } + + [Required(ErrorMessage = "La fecha de Vigencia Desde es obligatoria.")] + public DateTime VigenciaD { get; set; } + + [Required(ErrorMessage = "El porcentaje es obligatorio.")] + [Range(0, 100, ErrorMessage = "El porcentaje debe estar entre 0 y 100.")] // Asumiendo que es un porcentaje + public decimal Porcentaje { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePubliSeccionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePubliSeccionDto.cs new file mode 100644 index 0000000..032dd1c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePubliSeccionDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreatePubliSeccionDto + { + [Required] + public int IdPublicacion { get; set; } + + [Required(ErrorMessage = "El nombre de la sección es obligatorio.")] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + + public bool Estado { get; set; } = true; // Por defecto activa al crear + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateRecargoZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateRecargoZonaDto.cs new file mode 100644 index 0000000..fa33308 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateRecargoZonaDto.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateRecargoZonaDto + { + [Required(ErrorMessage = "El ID de la publicación es obligatorio.")] + public int IdPublicacion { get; set; } + + [Required(ErrorMessage = "La zona es obligatoria.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar una zona válida.")] + public int IdZona { get; set; } + + [Required(ErrorMessage = "La fecha de Vigencia Desde es obligatoria.")] + public DateTime VigenciaD { get; set; } + + [Required(ErrorMessage = "El valor del recargo es obligatorio.")] + [Range(0, 999999.99, ErrorMessage = "El valor del recargo debe ser un monto positivo.")] + public decimal Valor { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateSalidaOtroDestinoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateSalidaOtroDestinoDto.cs new file mode 100644 index 0000000..b830986 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateSalidaOtroDestinoDto.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateSalidaOtroDestinoDto + { + [Required] + public int IdPublicacion { get; set; } + [Required] + public int IdDestino { get; set; } + [Required] + public DateTime Fecha { get; set; } + [Required, Range(1, int.MaxValue)] + public int Cantidad { get; set; } + [StringLength(150)] + public string? Observacion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaDistDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaDistDto.cs new file mode 100644 index 0000000..7e478d1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaDistDto.cs @@ -0,0 +1,20 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class EntradaSalidaDistDto + { + public int IdParte { get; set; } + public int IdPublicacion { get; set; } + public string NombrePublicacion { get; set; } = string.Empty; + public string NombreEmpresaPublicacion { get; set; } = string.Empty; // Para identificar la empresa dueña del saldo + public int IdEmpresaPublicacion { get; set; } // Para afectar el saldo correcto + public int IdDistribuidor { get; set; } + public string NombreDistribuidor { get; set; } = string.Empty; + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public string TipoMovimiento { get; set; } = string.Empty; // "Salida" o "Entrada" + public int Cantidad { get; set; } + public int Remito { get; set; } + public string? Observacion { get; set; } + public decimal MontoCalculado { get; set; } // Calculado por el backend + // No exponemos Ids de Precio, Recargo, Porcentaje directamente, solo el resultado. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PorcMonCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PorcMonCanillaDto.cs new file mode 100644 index 0000000..213634f --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PorcMonCanillaDto.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class PorcMonCanillaDto // Para Porcentaje/Monto de Pago de Canillitas + { + public int IdPorcMon { get; set; } + public int IdPublicacion { get; set; } + public int IdCanilla { get; set; } + public string NomApeCanilla { get; set; } = string.Empty; // Para mostrar en UI + public string VigenciaD { get; set; } = string.Empty; // yyyy-MM-dd + public string? VigenciaH { get; set; } // yyyy-MM-dd + public decimal PorcMon { get; set; } // Valor (puede ser % o monto) + public bool EsPorcentaje { get; set; } // True si PorcMon es un %, False si es un monto fijo + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PorcPagoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PorcPagoDto.cs new file mode 100644 index 0000000..4860ee1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PorcPagoDto.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class PorcPagoDto // Para Porcentaje de Pago de Distribuidores + { + public int IdPorcentaje { get; set; } + public int IdPublicacion { get; set; } + // public string NombrePublicacion { get; set; } = string.Empty; // Opcional, si se necesita en una vista general de porcentajes + public int IdDistribuidor { get; set; } + public string NombreDistribuidor { get; set; } = string.Empty; // Para mostrar en UI + public string VigenciaD { get; set; } = string.Empty; // yyyy-MM-dd + public string? VigenciaH { get; set; } // yyyy-MM-dd + public decimal Porcentaje { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PubliSeccionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PubliSeccionDto.cs new file mode 100644 index 0000000..8d287d5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PubliSeccionDto.cs @@ -0,0 +1,11 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class PubliSeccionDto + { + public int IdSeccion { get; set; } + public int IdPublicacion { get; set; } + // public string NombrePublicacion { get; set; } = string.Empty; // Opcional si se muestra en una lista global + public string Nombre { get; set; } = string.Empty; // Nombre de la sección + public bool Estado { get; set; } // Habilitada o no + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/RecargoZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/RecargoZonaDto.cs new file mode 100644 index 0000000..539f44c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/RecargoZonaDto.cs @@ -0,0 +1,13 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class RecargoZonaDto + { + public int IdRecargo { get; set; } + public int IdPublicacion { get; set; } + public int IdZona { get; set; } + public string NombreZona { get; set; } = string.Empty; // Para mostrar en UI + public string VigenciaD { get; set; } = string.Empty; // yyyy-MM-dd + public string? VigenciaH { get; set; } // yyyy-MM-dd + public decimal Valor { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/SalidaOtroDestinoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/SalidaOtroDestinoDto.cs new file mode 100644 index 0000000..80e8a37 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/SalidaOtroDestinoDto.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class SalidaOtroDestinoDto + { + public int IdParte { get; set; } + public int IdPublicacion { get; set; } + public string NombrePublicacion { get; set; } = string.Empty; + public int IdDestino { get; set; } + public string NombreDestino { get; set; } = string.Empty; + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public int Cantidad { get; set; } + public string? Observacion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaDistDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaDistDto.cs new file mode 100644 index 0000000..7b3f00e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaDistDto.cs @@ -0,0 +1,15 @@ +// La edición de una entrada/salida puede ser compleja por la afectación de saldos. +// Por ahora, podríamos permitir editar Cantidad y Observacion si el TipoMovimiento no cambia. +// Cambiar Publicacion, Distribuidor, Fecha o TipoMovimiento podría requerir anular y recrear. +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateEntradaSalidaDistDto + { + [Required, Range(1, int.MaxValue)] + public int Cantidad { get; set; } + [StringLength(150)] + public string? Observacion { get; set; } + // No se permite cambiar TipoMovimiento, Fecha, Publicacion, Distribuidor, Remito aquí. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePorcMonCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePorcMonCanillaDto.cs new file mode 100644 index 0000000..678ece6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePorcMonCanillaDto.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdatePorcMonCanillaDto + { + // IdPublicacion, IdCanilla y VigenciaD no deberían cambiar. + // Solo se actualiza PorcMon, EsPorcentaje y opcionalmente se cierra con VigenciaH. + + [Required(ErrorMessage = "El valor (porcentaje o monto) es obligatorio.")] + [Range(0, double.MaxValue, ErrorMessage = "El valor debe ser un monto positivo.")] + public decimal PorcMon { get; set; } + + [Required] + public bool EsPorcentaje { get; set; } + + public DateTime? VigenciaH { get; set; } // Para cerrar el período + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePorcPagoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePorcPagoDto.cs new file mode 100644 index 0000000..1a4d375 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePorcPagoDto.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdatePorcPagoDto + { + // IdPublicacion, IdDistribuidor y VigenciaD no deberían cambiar. + // Si cambian, se considera un nuevo registro. + // Solo se actualiza el Porcentaje y opcionalmente se cierra con VigenciaH. + + [Required(ErrorMessage = "El porcentaje es obligatorio.")] + [Range(0, 100, ErrorMessage = "El porcentaje debe estar entre 0 y 100.")] + public decimal Porcentaje { get; set; } + + public DateTime? VigenciaH { get; set; } // Para cerrar el período + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePubliSeccionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePubliSeccionDto.cs new file mode 100644 index 0000000..1eac702 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePubliSeccionDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdatePubliSeccionDto + { + [Required(ErrorMessage = "El nombre de la sección es obligatorio.")] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + + [Required] + public bool Estado { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateRecargoZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateRecargoZonaDto.cs new file mode 100644 index 0000000..869f04f --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateRecargoZonaDto.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateRecargoZonaDto // Para actualizar un IdRecargo específico + { + // IdPublicacion, IdZona y VigenciaD no deberían cambiar. Si cambian, es un nuevo registro. + // Solo se actualiza Valor y opcionalmente se cierra con VigenciaH. + + [Required(ErrorMessage = "El valor del recargo es obligatorio.")] + [Range(0, 999999.99, ErrorMessage = "El valor del recargo debe ser un monto positivo.")] + public decimal Valor { get; set; } + + public DateTime? VigenciaH { get; set; } // Para cerrar el período + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateSalidaOtroDestinoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateSalidaOtroDestinoDto.cs new file mode 100644 index 0000000..b6377f3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateSalidaOtroDestinoDto.cs @@ -0,0 +1,14 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateSalidaOtroDestinoDto + { + // IdPublicacion, IdDestino y Fecha usualmente no se cambian en una "salida" ya registrada. + // Se podría permitir cambiar cantidad y observación. + [Required, Range(1, int.MaxValue)] + public int Cantidad { get; set; } + [StringLength(150)] + public string? Observacion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CambiarEstadoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CambiarEstadoBobinaDto.cs new file mode 100644 index 0000000..25e6aae --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CambiarEstadoBobinaDto.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CambiarEstadoBobinaDto + { + [Required] + public int NuevoEstadoId { get; set; } // ID del nuevo estado (ej. 2 para "En Uso", 3 para "Dañada") + + public int? IdPublicacion { get; set; } // Requerido si NuevoEstadoId es "En Uso" + public int? IdSeccion { get; set; } // Requerido si NuevoEstadoId es "En Uso" + + [StringLength(250)] + public string? Obs { get; set; } + + [Required] + public DateTime FechaCambioEstado { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaDto.cs new file mode 100644 index 0000000..56fa623 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaDto.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CreateStockBobinaDto + { + [Required] + public int IdTipoBobina { get; set; } + [Required, StringLength(15)] + public string NroBobina { get; set; } = string.Empty; + [Required, Range(1, int.MaxValue)] + public int Peso { get; set; } + [Required] + public int IdPlanta { get; set; } + [Required, StringLength(15)] + public string Remito { get; set; } = string.Empty; + [Required] + public DateTime FechaRemito { get; set; } + // IdEstadoBobina se setea a "Disponible" (1) en el backend + // FechaEstado se setea a FechaRemito en el backend + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateTiradaRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateTiradaRequestDto.cs new file mode 100644 index 0000000..9b3378d --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateTiradaRequestDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CreateTiradaRequestDto + { + [Required] + public int IdPublicacion { get; set; } + [Required] + public DateTime Fecha { get; set; } + [Required] + public int IdPlanta { get; set; } + [Required, Range(1, int.MaxValue)] + public int Ejemplares { get; set; } // Para bob_RegTiradas + + [Required] + [MinLength(1, ErrorMessage = "Debe especificar al menos una sección para la tirada.")] + public List Secciones { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/DetalleSeccionTiradaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/DetalleSeccionTiradaDto.cs new file mode 100644 index 0000000..bdff6b6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/DetalleSeccionTiradaDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class DetalleSeccionTiradaDto + { + public int IdSeccion { get; set; } // ID de la PubliSeccion + // public string NombreSeccion { get; set; } // Opcional, para mostrar en UI de creación/edición + [Required, Range(1, 1000)] // Ajustar rango según necesidad + public int CantPag { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/StockBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/StockBobinaDto.cs new file mode 100644 index 0000000..7505318 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/StockBobinaDto.cs @@ -0,0 +1,23 @@ +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class StockBobinaDto + { + public int IdBobina { get; set; } + public int IdTipoBobina { get; set; } + public string NombreTipoBobina { get; set; } = string.Empty; + public string NroBobina { get; set; } = string.Empty; + public int Peso { get; set; } + public int IdPlanta { get; set; } + public string NombrePlanta { get; set; } = string.Empty; + public int IdEstadoBobina { get; set; } + public string NombreEstadoBobina { get; set; } = string.Empty; + public string Remito { get; set; } = string.Empty; + public string FechaRemito { get; set; } = string.Empty; // yyyy-MM-dd + public string? FechaEstado { get; set; } // yyyy-MM-dd + public int? IdPublicacion { get; set; } + public string? NombrePublicacion { get; set; } + public int? IdSeccion { get; set; } + public string? NombreSeccion { get; set; } + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/TiradaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/TiradaDto.cs new file mode 100644 index 0000000..8cb24d4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/TiradaDto.cs @@ -0,0 +1,23 @@ +// Este DTO es más complejo para mostrar la información de forma útil. +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class DetalleSeccionEnListadoDto + { + public int IdSeccion { get; set; } + public string NombreSeccion { get; set; } = string.Empty; + public int CantPag { get; set; } + public int IdRegPublicacionSeccion {get;set;} // Id_Tirada de bob_RegPublicaciones + } + public class TiradaDto + { + public int IdRegistroTirada { get; set; } // Id de bob_RegTiradas + public int IdPublicacion { get; set; } + public string NombrePublicacion { get; set; } = string.Empty; + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public int IdPlanta { get; set; } + public string NombrePlanta { get; set; } = string.Empty; + public int Ejemplares { get; set; } + public List SeccionesImpresas { get; set; } = new List(); + public int TotalPaginasSumadas { get; set; } // Suma de CantPag de las secciones impresas + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateStockBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateStockBobinaDto.cs new file mode 100644 index 0000000..852fe1b --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateStockBobinaDto.cs @@ -0,0 +1,23 @@ +// (Para editar datos básicos de una bobina *Disponible*) +// Los cambios de estado tendrán DTOs dedicados. +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class UpdateStockBobinaDto + { + [Required] + public int IdTipoBobina { get; set; } + [Required, StringLength(15)] + public string NroBobina { get; set; } = string.Empty; + [Required, Range(1, int.MaxValue)] + public int Peso { get; set; } + [Required] + public int IdPlanta { get; set; } + [Required, StringLength(15)] + public string Remito { get; set; } = string.Empty; + [Required] + public DateTime FechaRemito { get; set; } + // No se puede cambiar el estado desde aquí directamente + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/RegPublicacionSeccion.cs b/Backend/GestionIntegral.Api/Models/Impresion/RegPublicacionSeccion.cs new file mode 100644 index 0000000..74de80d --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/RegPublicacionSeccion.cs @@ -0,0 +1,12 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class RegPublicacionSeccion + { + public int IdTirada { get; set; } // Id_Tirada (PK, Identity en bob_RegPublicaciones) + public int IdPublicacion { get; set; } + public int IdSeccion { get; set; } + public int CantPag { get; set; } + public DateTime Fecha { get; set; } + public int IdPlanta { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/RegPublicacionSeccionHistorico.cs b/Backend/GestionIntegral.Api/Models/Impresion/RegPublicacionSeccionHistorico.cs new file mode 100644 index 0000000..5770828 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/RegPublicacionSeccionHistorico.cs @@ -0,0 +1,15 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class RegPublicacionSeccionHistorico + { + public int Id_Tirada { get; set; } // Nombre de columna en _H + public int Id_Publicacion { get; set; } + public int Id_Seccion { get; set; } + public int CantPag { get; set; } + public DateTime Fecha { get; set; } + public int Id_Planta { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/RegTirada.cs b/Backend/GestionIntegral.Api/Models/Impresion/RegTirada.cs new file mode 100644 index 0000000..eb75337 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/RegTirada.cs @@ -0,0 +1,11 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class RegTirada + { + public int IdRegistro { get; set; } // Id_Registro (PK, Identity) + public int Ejemplares { get; set; } + public int IdPublicacion { get; set; } + public DateTime Fecha { get; set; } + public int IdPlanta { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/RegTiradaHistorico.cs b/Backend/GestionIntegral.Api/Models/Impresion/RegTiradaHistorico.cs new file mode 100644 index 0000000..a4d7bbb --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/RegTiradaHistorico.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class RegTiradaHistorico + { + public int Id_Registro { get; set; } // Nombre de columna en _H + public int Ejemplares { get; set; } + public int Id_Publicacion { get; set; } + public DateTime Fecha { get; set; } + public int Id_Planta { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/StockBobina.cs b/Backend/GestionIntegral.Api/Models/Impresion/StockBobina.cs new file mode 100644 index 0000000..353b261 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/StockBobina.cs @@ -0,0 +1,20 @@ +using System; + +namespace GestionIntegral.Api.Models.Impresion +{ + public class StockBobina + { + public int IdBobina { get; set; } // Id_Bobina (PK, Identity) + public int IdTipoBobina { get; set; } // Id_TipoBobina (FK) + public string NroBobina { get; set; } = string.Empty; // NroBobina (varchar(15), NOT NULL) + public int Peso { get; set; } // Peso (int, NOT NULL) + public int IdPlanta { get; set; } // Id_Planta (FK) + public int IdEstadoBobina { get; set; } // Id_EstadoBobina (FK) + public string Remito { get; set; } = string.Empty; // Remito (varchar(15), NOT NULL) + public DateTime FechaRemito { get; set; } // FechaRemito (datetime2(0), NOT NULL) + public DateTime? FechaEstado { get; set; } // FechaEstado (datetime2(0), NULL) + public int? IdPublicacion { get; set; } // Id_Publicacion (FK, NULL) + public int? IdSeccion { get; set; } // Id_Seccion (FK, NULL) + public string? Obs { get; set; } // Obs (varchar(250), NULL) + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/StockBobinaHistorico.cs b/Backend/GestionIntegral.Api/Models/Impresion/StockBobinaHistorico.cs new file mode 100644 index 0000000..88d76b6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/StockBobinaHistorico.cs @@ -0,0 +1,25 @@ +using System; + +namespace GestionIntegral.Api.Models.Impresion +{ + public class StockBobinaHistorico + { + public int Id_Bobina { get; set; } // Columna en _H + public int Id_TipoBobina { get; set; } + public string NroBobina { get; set; } = string.Empty; + public int Peso { get; set; } + public int Id_Planta { get; set; } + public int Id_EstadoBobina { get; set; } + public string Remito { get; set; } = string.Empty; + public DateTime FechaRemito { get; set; } + public DateTime? FechaEstado { get; set; } + public int? Id_Publicacion { get; set; } + public int? Id_Seccion { get; set; } + public string? Obs { get; set; } + + // Auditoría + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Ingreso", "Estado Cambiado", "Actualizacion", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index ddef612..960ddbc 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -10,6 +10,7 @@ using GestionIntegral.Api.Data.Repositories.Impresion; using GestionIntegral.Api.Services.Impresion; using GestionIntegral.Api.Services.Usuarios; using GestionIntegral.Api.Data.Repositories.Usuarios; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); @@ -46,11 +47,24 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // --- Configuración de Autenticación JWT --- var jwtSettings = builder.Configuration.GetSection("Jwt"); @@ -61,6 +75,7 @@ builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { @@ -82,7 +97,7 @@ builder.Services.AddAuthentication(options => // --- Configuración de Autorización --- builder.Services.AddAuthorization(); -// --- Configuración de CORS --- // <--- MOVER AQUÍ LA CONFIGURACIÓN DE SERVICIOS CORS +// --- Configuración de CORS --- // var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; builder.Services.AddCors(options => { @@ -101,26 +116,75 @@ builder.Services.AddCors(options => // --- Servicios del Contenedor --- builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +// --- Configuración Avanzada de Swagger/OpenAPI --- +builder.Services.AddSwaggerGen(options => +{ + // Definición general del documento Swagger + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "GestionIntegral API", + Description = "API para el sistema de Gestión Integral (Migración VB.NET)", + // Puedes añadir TermsOfService, Contact, License si lo deseas + }); + + // (Opcional) Incluir comentarios XML si los usas en tus controladores/DTOs + // var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + // options.IncludeXmlComments(System.IO.Path.Combine(AppContext.BaseDirectory, xmlFilename)); + + // Definición de Seguridad para JWT en Swagger UI + // Esto añade el botón "Authorize" en la UI de Swagger para poder pegar el token Bearer + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Por favor, introduce 'Bearer' seguido de un espacio y luego tu token JWT.", + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, // Usar ApiKey para el formato Bearer simple + Scheme = "Bearer" // Esquema a usar + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" // Debe coincidir con el Id definido en AddSecurityDefinition + }, + Scheme = "oauth2", // Aunque el scheme sea ApiKey, a veces se usa oauth2 aquí para la UI + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() // Lista de scopes (vacía si no usas scopes OAuth2 complejos) + } + }); +}); +// --- Fin Configuración Swagger --- var app = builder.Build(); // --- Configuración del Pipeline HTTP --- if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwagger(); // Habilita el middleware para servir el JSON de Swagger + app.UseSwaggerUI(options => // Habilita el middleware para servir la UI de Swagger + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "GestionIntegral API v1"); + // options.RoutePrefix = string.Empty; // Para servir la UI de Swagger en la raíz (ej: http://localhost:5183/) + // Comenta esto si prefieres /swagger (ej: http://localhost:5183/swagger/) + }); } // ¡¡¡NO USAR UseHttpsRedirection si tu API corre en HTTP!!! // Comenta o elimina la siguiente línea si SÓLO usas http://localhost:5183 -// app.UseHttpsRedirection(); // <--- COMENTAR/ELIMINAR SI NO USAS HTTPS EN API +// app.UseHttpsRedirection(); -// --- Aplicar CORS ANTES de Autenticación/Autorización --- app.UseCors(MyAllowSpecificOrigins); -// --- Fin aplicar CORS --- -app.UseAuthentication(); +app.UseAuthentication(); // Debe ir ANTES de UseAuthorization app.UseAuthorization(); app.MapControllers(); diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs new file mode 100644 index 0000000..9b10f03 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs @@ -0,0 +1,355 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Contables; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class EntradaSalidaDistService : IEntradaSalidaDistService + { + private readonly IEntradaSalidaDistRepository _esRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly IDistribuidorRepository _distribuidorRepository; + private readonly IPrecioRepository _precioRepository; + private readonly IRecargoZonaRepository _recargoZonaRepository; + private readonly IPorcPagoRepository _porcPagoRepository; + private readonly ISaldoRepository _saldoRepository; + private readonly IEmpresaRepository _empresaRepository; // Para obtener IdEmpresa de la publicación + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public EntradaSalidaDistService( + IEntradaSalidaDistRepository esRepository, + IPublicacionRepository publicacionRepository, + IDistribuidorRepository distribuidorRepository, + IPrecioRepository precioRepository, + IRecargoZonaRepository recargoZonaRepository, + IPorcPagoRepository porcPagoRepository, + ISaldoRepository saldoRepository, + IEmpresaRepository empresaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _esRepository = esRepository; + _publicacionRepository = publicacionRepository; + _distribuidorRepository = distribuidorRepository; + _precioRepository = precioRepository; + _recargoZonaRepository = recargoZonaRepository; + _porcPagoRepository = porcPagoRepository; + _saldoRepository = saldoRepository; + _empresaRepository = empresaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task MapToDto(EntradaSalidaDist es) + { + if (es == null) return null!; + + var publicacionData = await _publicacionRepository.GetByIdAsync(es.IdPublicacion); + var distribuidorData = await _distribuidorRepository.GetByIdAsync(es.IdDistribuidor); + + decimal montoCalculado = await CalcularMontoMovimiento(es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad, es.TipoMovimiento, + es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona); + + + return new EntradaSalidaDistDto + { + IdParte = es.IdParte, + IdPublicacion = es.IdPublicacion, + NombrePublicacion = publicacionData.Publicacion?.Nombre ?? "N/A", + IdEmpresaPublicacion = publicacionData.Publicacion?.IdEmpresa ?? 0, + NombreEmpresaPublicacion = publicacionData.NombreEmpresa ?? "N/A", + IdDistribuidor = es.IdDistribuidor, + NombreDistribuidor = distribuidorData.Distribuidor?.Nombre ?? "N/A", + Fecha = es.Fecha.ToString("yyyy-MM-dd"), + TipoMovimiento = es.TipoMovimiento, + Cantidad = es.Cantidad, + Remito = es.Remito, + Observacion = es.Observacion, + MontoCalculado = montoCalculado + }; + } + + private async Task CalcularMontoMovimiento(int idPublicacion, int idDistribuidor, DateTime fecha, int cantidad, string tipoMovimiento, + int idPrecio, int idRecargo, int idPorcentaje, int? idZonaDistribuidor) + { + if (tipoMovimiento == "Entrada") return 0; // Las entradas (devoluciones) no generan "debe" inmediato, ajustan el saldo. + + var precioConfig = await _precioRepository.GetByIdAsync(idPrecio); + if (precioConfig == null) throw new InvalidOperationException("Configuración de precio no encontrada para el movimiento."); + + decimal precioDia = 0; + DayOfWeek diaSemana = fecha.DayOfWeek; + switch (diaSemana) + { + case DayOfWeek.Monday: precioDia = precioConfig.Lunes ?? 0; break; + case DayOfWeek.Tuesday: precioDia = precioConfig.Martes ?? 0; break; + case DayOfWeek.Wednesday: precioDia = precioConfig.Miercoles ?? 0; break; + case DayOfWeek.Thursday: precioDia = precioConfig.Jueves ?? 0; break; + case DayOfWeek.Friday: precioDia = precioConfig.Viernes ?? 0; break; + case DayOfWeek.Saturday: precioDia = precioConfig.Sabado ?? 0; break; + case DayOfWeek.Sunday: precioDia = precioConfig.Domingo ?? 0; break; + } + + decimal valorRecargo = 0; + if (idRecargo > 0 && idZonaDistribuidor.HasValue) // El recargo se aplica por la zona del distribuidor + { + // Necesitamos encontrar el recargo activo para la publicación, la zona del distribuidor y la fecha + var recargoConfig = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(idPublicacion, idZonaDistribuidor.Value, fecha); + if (recargoConfig != null) + { + valorRecargo = recargoConfig.Valor; + } + } + + decimal precioConRecargo = precioDia + valorRecargo; + decimal montoBase = precioConRecargo * cantidad; + + if (idPorcentaje > 0) + { + var porcConfig = await _porcPagoRepository.GetByIdAsync(idPorcentaje); + if (porcConfig != null) + { + return (montoBase / 100) * porcConfig.Porcentaje; + } + } + return montoBase; // Si no hay porcentaje, se factura el 100% del precio con recargo + } + + + public async Task> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento) + { + var movimientos = await _esRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idDistribuidor, tipoMovimiento); + var dtos = new List(); + foreach (var mov in movimientos) + { + dtos.Add(await MapToDto(mov)); + } + return dtos; + } + + public async Task ObtenerPorIdAsync(int idParte) + { + var movimiento = await _esRepository.GetByIdAsync(idParte); + return movimiento == null ? null : await MapToDto(movimiento); + } + + public async Task<(EntradaSalidaDistDto? Movimiento, string? Error)> CrearMovimientoAsync(CreateEntradaSalidaDistDto createDto, int idUsuario) + { + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion); + if (publicacion == null || publicacion.Habilitada != true) return (null, "Publicación no válida o no habilitada."); + + var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor); + if (distribuidor == null) return (null, "Distribuidor no válido."); + + if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion)) + { + return (null, $"Ya existe un movimiento de '{createDto.TipoMovimiento}' con el remito N°{createDto.Remito} para esta publicación."); + } + + // Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento + var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date); + if (precioActivo == null) return (null, $"No hay un precio definido para la publicación '{publicacion.Nombre}' en la fecha {createDto.Fecha:dd/MM/yyyy}."); + + RecargoZona? recargoActivo = null; + if (distribuidor.IdZona.HasValue) + { + recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(createDto.IdPublicacion, distribuidor.IdZona.Value, createDto.Fecha.Date); + } + + var porcPagoActivo = await _porcPagoRepository.GetActiveByPublicacionDistribuidorAndDateAsync(createDto.IdPublicacion, createDto.IdDistribuidor, createDto.Fecha.Date); + + var nuevoES = new EntradaSalidaDist + { + IdPublicacion = createDto.IdPublicacion, + IdDistribuidor = createDto.IdDistribuidor, + Fecha = createDto.Fecha.Date, + TipoMovimiento = createDto.TipoMovimiento, + Cantidad = createDto.Cantidad, + Remito = createDto.Remito, + Observacion = createDto.Observacion, + IdPrecio = precioActivo.IdPrecio, + IdRecargo = recargoActivo?.IdRecargo ?? 0, // 0 si no aplica + IdPorcentaje = porcPagoActivo?.IdPorcentaje ?? 0 // 0 si no aplica + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var esCreada = await _esRepository.CreateAsync(nuevoES, idUsuario, transaction); + if (esCreada == null) throw new DataException("Error al registrar el movimiento."); + + // Afectar Saldo + decimal montoAfectacion = await CalcularMontoMovimiento( + esCreada.IdPublicacion, esCreada.IdDistribuidor, esCreada.Fecha, esCreada.Cantidad, esCreada.TipoMovimiento, + esCreada.IdPrecio, esCreada.IdRecargo, esCreada.IdPorcentaje, distribuidor.IdZona); + + // Si es Salida, montoAfectacion es positivo (aumenta deuda). Si es Entrada, es 0 por CalcularMontoMovimiento, + // pero para el saldo, una Entrada (devolución) debería restar del saldo deudor. + // Por lo tanto, el monto real a restar del saldo si es Entrada sería el valor de esos ejemplares devueltos. + if(esCreada.TipoMovimiento == "Entrada") + { + // Recalcular como si fuera salida para obtener el valor a acreditar/restar del saldo + montoAfectacion = await CalcularMontoMovimiento( + esCreada.IdPublicacion, esCreada.IdDistribuidor, esCreada.Fecha, esCreada.Cantidad, "Salida", // Forzar tipo Salida para cálculo de valor + esCreada.IdPrecio, esCreada.IdRecargo, esCreada.IdPorcentaje, distribuidor.IdZona); + montoAfectacion *= -1; // Hacerlo negativo para restar del saldo + } + + + bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esCreada.IdDistribuidor, publicacion.IdEmpresa, montoAfectacion, transaction); + if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor."); + + transaction.Commit(); + _logger.LogInformation("Movimiento ID {Id} creado y saldo afectado por Usuario ID {UserId}.", esCreada.IdParte, idUsuario); + return (await MapToDto(esCreada), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearMovimientoAsync para Pub ID {IdPub}", createDto.IdPublicacion); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaDistDto updateDto, int idUsuario) + { + // La actualización de un movimiento que afecta saldos es compleja. + // Si cambia la cantidad, el monto original y el nuevo deben calcularse, + // y la diferencia debe aplicarse al saldo. + // Por ahora, este DTO solo permite cambiar Cantidad y Observacion. + // Cambiar otros campos como Fecha, Publicacion, Distribuidor implicaría recalcular todo + // y posiblemente anular el movimiento original y crear uno nuevo. + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var esExistente = await _esRepository.GetByIdAsync(idParte); + if (esExistente == null) return (false, "Movimiento no encontrado."); + + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(esExistente.IdPublicacion); + if (publicacion == null) return (false, "Publicación asociada no encontrada."); // Muy raro + var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor); + if (distribuidor == null) return (false, "Distribuidor asociado no encontrado."); + + + // 1. Calcular monto del movimiento original (antes de la actualización) + decimal montoOriginal = await CalcularMontoMovimiento( + esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento, + esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona); + if(esExistente.TipoMovimiento == "Entrada") montoOriginal *= -1; // Para revertir + + // 2. Actualizar la entidad en la BD (esto también guarda en historial) + var esParaActualizar = new EntradaSalidaDist + { + IdParte = esExistente.IdParte, + IdPublicacion = esExistente.IdPublicacion, // No cambia + IdDistribuidor = esExistente.IdDistribuidor, // No cambia + Fecha = esExistente.Fecha, // No cambia + TipoMovimiento = esExistente.TipoMovimiento, // No cambia + Cantidad = updateDto.Cantidad, // Nuevo valor + Remito = esExistente.Remito, // No cambia + Observacion = updateDto.Observacion, // Nuevo valor + IdPrecio = esExistente.IdPrecio, // No cambia + IdRecargo = esExistente.IdRecargo, // No cambia + IdPorcentaje = esExistente.IdPorcentaje // No cambia + }; + var actualizado = await _esRepository.UpdateAsync(esParaActualizar, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar el movimiento."); + + // 3. Calcular monto del movimiento nuevo (después de la actualización) + decimal montoNuevo = await CalcularMontoMovimiento( + esParaActualizar.IdPublicacion, esParaActualizar.IdDistribuidor, esParaActualizar.Fecha, esParaActualizar.Cantidad, esParaActualizar.TipoMovimiento, + esParaActualizar.IdPrecio, esParaActualizar.IdRecargo, esParaActualizar.IdPorcentaje, distribuidor.IdZona); + if(esParaActualizar.TipoMovimiento == "Entrada") montoNuevo *= -1; + + + // 4. Ajustar saldo con la diferencia + decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal; + if (diferenciaAjusteSaldo != 0) + { + bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, diferenciaAjusteSaldo, transaction); + if (!saldoActualizado) throw new DataException("Error al ajustar el saldo del distribuidor tras la actualización."); + } + + transaction.Commit(); + _logger.LogInformation("Movimiento ID {Id} actualizado y saldo ajustado por Usuario ID {UserId}.", idParte, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarMovimientoAsync ID: {Id}", idParte); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, 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(); + try + { + var esExistente = await _esRepository.GetByIdAsync(idParte); + if (esExistente == null) return (false, "Movimiento no encontrado."); + + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(esExistente.IdPublicacion); + if (publicacion == null) return (false, "Publicación asociada no encontrada."); + var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor); + if (distribuidor == null) return (false, "Distribuidor asociado no encontrado."); + + // 1. Calcular el monto del movimiento a eliminar para revertir el saldo + decimal montoReversion = await CalcularMontoMovimiento( + esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento, + esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona); + + // Si es Salida, el monto es positivo, al revertir restamos. + // Si es Entrada, el monto es 0 (por Calcular...), pero su efecto en saldo fue negativo, al revertir sumamos el valor. + if(esExistente.TipoMovimiento == "Salida") { + montoReversion *= -1; // Para restar del saldo lo que se sumó + } else if (esExistente.TipoMovimiento == "Entrada") { + // Recalcular el valor como si fuera salida para saber cuánto se restó del saldo + montoReversion = await CalcularMontoMovimiento( + esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, "Salida", + esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona); + // No se multiplica por -1 aquí, porque el saldo ya lo tiene restado, al eliminar revertimos sumando. + } + + + // 2. Eliminar el registro (esto también guarda en historial) + var eliminado = await _esRepository.DeleteAsync(idParte, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar el movimiento."); + + // 3. Ajustar Saldo + bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, montoReversion, transaction); + if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación."); + + transaction.Commit(); + _logger.LogInformation("Movimiento ID {Id} eliminado y saldo revertido por Usuario ID {UserId}.", idParte, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarMovimientoAsync ID: {Id}", idParte); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaDistService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaDistService.cs new file mode 100644 index 0000000..ecce864 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaDistService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IEntradaSalidaDistService + { + Task> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento); + Task ObtenerPorIdAsync(int idParte); + Task<(EntradaSalidaDistDto? Movimiento, string? Error)> CrearMovimientoAsync(CreateEntradaSalidaDistDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaDistDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IPorcMonCanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IPorcMonCanillaService.cs new file mode 100644 index 0000000..c3def7c --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IPorcMonCanillaService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IPorcMonCanillaService + { + Task> ObtenerPorPublicacionIdAsync(int idPublicacion); + Task ObtenerPorIdAsync(int idPorcMon); + Task<(PorcMonCanillaDto? Item, string? Error)> CrearAsync(CreatePorcMonCanillaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idPorcMon, UpdatePorcMonCanillaDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idPorcMon, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IPorcPagoService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IPorcPagoService.cs new file mode 100644 index 0000000..79b0ae3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IPorcPagoService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IPorcPagoService + { + Task> ObtenerPorPublicacionIdAsync(int idPublicacion); + Task ObtenerPorIdAsync(int idPorcentaje); + Task<(PorcPagoDto? PorcPago, string? Error)> CrearAsync(CreatePorcPagoDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idPorcentaje, UpdatePorcPagoDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idPorcentaje, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IPubliSeccionService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IPubliSeccionService.cs new file mode 100644 index 0000000..9e832d0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IPubliSeccionService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IPubliSeccionService + { + Task> ObtenerPorPublicacionIdAsync(int idPublicacion, bool? soloActivas = null); + Task ObtenerPorIdAsync(int idSeccion); + Task<(PubliSeccionDto? Seccion, string? Error)> CrearAsync(CreatePubliSeccionDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idSeccion, UpdatePubliSeccionDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idSeccion, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IRecargoZonaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IRecargoZonaService.cs new file mode 100644 index 0000000..ef1294f --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IRecargoZonaService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IRecargoZonaService + { + Task> ObtenerPorPublicacionIdAsync(int idPublicacion); + Task ObtenerPorIdAsync(int idRecargo); + Task<(RecargoZonaDto? Recargo, string? Error)> CrearAsync(CreateRecargoZonaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idRecargo, UpdateRecargoZonaDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idRecargo, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/ISalidaOtroDestinoService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/ISalidaOtroDestinoService.cs new file mode 100644 index 0000000..f4e2a56 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/ISalidaOtroDestinoService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface ISalidaOtroDestinoService + { + Task> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino); + Task ObtenerPorIdAsync(int idParte); + Task<(SalidaOtroDestinoDto? Salida, string? Error)> CrearAsync(CreateSalidaOtroDestinoDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idParte, UpdateSalidaOtroDestinoDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idParte, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/PorcMonCanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/PorcMonCanillaService.cs new file mode 100644 index 0000000..5d80d7c --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/PorcMonCanillaService.cs @@ -0,0 +1,240 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class PorcMonCanillaService : IPorcMonCanillaService + { + private readonly IPorcMonCanillaRepository _porcMonCanillaRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly ICanillaRepository _canillaRepository; // Para validar IdCanilla y obtener nombre + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PorcMonCanillaService( + IPorcMonCanillaRepository porcMonCanillaRepository, + IPublicacionRepository publicacionRepository, + ICanillaRepository canillaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _porcMonCanillaRepository = porcMonCanillaRepository; + _publicacionRepository = publicacionRepository; + _canillaRepository = canillaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private PorcMonCanillaDto MapToDto((PorcMonCanilla Item, string NomApeCanilla) data) => new PorcMonCanillaDto + { + IdPorcMon = data.Item.IdPorcMon, + IdPublicacion = data.Item.IdPublicacion, + IdCanilla = data.Item.IdCanilla, + NomApeCanilla = data.NomApeCanilla, + VigenciaD = data.Item.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = data.Item.VigenciaH?.ToString("yyyy-MM-dd"), + PorcMon = data.Item.PorcMon, + EsPorcentaje = data.Item.EsPorcentaje + }; + + private async Task MapToDtoWithLookup(PorcMonCanilla? item) + { + if (item == null) return null; // Si el item es null, devuelve null + + var canillaData = await _canillaRepository.GetByIdAsync(item.IdCanilla); + return new PorcMonCanillaDto + { + IdPorcMon = item.IdPorcMon, + IdPublicacion = item.IdPublicacion, + IdCanilla = item.IdCanilla, + NomApeCanilla = canillaData.Canilla?.NomApe ?? "Canillita Desconocido", + VigenciaD = item.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = item.VigenciaH?.ToString("yyyy-MM-dd"), + PorcMon = item.PorcMon, + EsPorcentaje = item.EsPorcentaje + }; + } + + + public async Task> ObtenerPorPublicacionIdAsync(int idPublicacion) + { + var data = await _porcMonCanillaRepository.GetByPublicacionIdAsync(idPublicacion); + // Filtrar los nulos que MapToDto podría devolver (aunque no debería en este caso si GetAllWithProfileNameAsync no devuelve usuarios nulos en la tupla) + return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); + } + + public async Task ObtenerPorIdAsync(int idPorcMon) + { + var item = await _porcMonCanillaRepository.GetByIdAsync(idPorcMon); + return await MapToDtoWithLookup(item); + } + + public async Task<(PorcMonCanillaDto? Item, string? Error)> CrearAsync(CreatePorcMonCanillaDto createDto, int idUsuario) + { + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación especificada no existe."); + + var canillaData = await _canillaRepository.GetByIdAsync(createDto.IdCanilla); + if (canillaData.Canilla == null) // GetByIdAsync devuelve una tupla + return (null, "El canillita especificado no existe o no está activo."); + // Validar que solo canillitas accionistas pueden tener porcentaje/monto (o la regla de negocio que aplique) + // Por ejemplo, si solo los Accionistas pueden tener esta configuración: + // if (!canillaData.Canilla.Accionista) { + // return (null, "Solo los canillitas accionistas pueden tener un porcentaje/monto de pago configurado."); + // } + + + var itemActivo = await _porcMonCanillaRepository.GetActiveByPublicacionCanillaAndDateAsync(createDto.IdPublicacion, createDto.IdCanilla, createDto.VigenciaD.Date); + if (itemActivo != null) + { + return (null, $"Ya existe un porcentaje/monto activo para esta publicación y canillita en la fecha {createDto.VigenciaD:dd/MM/yyyy}. Cierre el período anterior."); + } + + var nuevoItem = new PorcMonCanilla + { + IdPublicacion = createDto.IdPublicacion, + IdCanilla = createDto.IdCanilla, + VigenciaD = createDto.VigenciaD.Date, + VigenciaH = null, + PorcMon = createDto.PorcMon, + EsPorcentaje = createDto.EsPorcentaje + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var itemAnterior = await _porcMonCanillaRepository.GetPreviousActiveAsync(createDto.IdPublicacion, createDto.IdCanilla, nuevoItem.VigenciaD, transaction); + if (itemAnterior != null) + { + if (itemAnterior.VigenciaD.Date >= nuevoItem.VigenciaD.Date) + { + transaction.Rollback(); + return (null, $"La fecha de inicio ({nuevoItem.VigenciaD:dd/MM/yyyy}) no puede ser anterior o igual a la del último período vigente ({itemAnterior.VigenciaD:dd/MM/yyyy})."); + } + itemAnterior.VigenciaH = nuevoItem.VigenciaD.AddDays(-1); + await _porcMonCanillaRepository.UpdateAsync(itemAnterior, idUsuario, transaction); + } + + var itemCreado = await _porcMonCanillaRepository.CreateAsync(nuevoItem, idUsuario, transaction); + if (itemCreado == null) throw new DataException("Error al crear el registro."); + + transaction.Commit(); + _logger.LogInformation("PorcMonCanilla ID {Id} creado por Usuario ID {UserId}.", itemCreado.IdPorcMon, idUsuario); + return (await MapToDtoWithLookup(itemCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync PorcMonCanilla para Pub ID {IdPub}, Canilla ID {IdCan}", createDto.IdPublicacion, createDto.IdCanilla); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPorcMon, UpdatePorcMonCanillaDto 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(); + try + { + var itemExistente = await _porcMonCanillaRepository.GetByIdAsync(idPorcMon); + if (itemExistente == null) return (false, "Registro de porcentaje/monto no encontrado."); + + if (updateDto.VigenciaH.HasValue && updateDto.VigenciaH.Value.Date < itemExistente.VigenciaD.Date) + return (false, "Vigencia Hasta no puede ser anterior a Vigencia Desde."); + + if (updateDto.VigenciaH.HasValue) + { + var itemsPubCanillaData = await _porcMonCanillaRepository.GetByPublicacionIdAsync(itemExistente.IdPublicacion); + var itemsPosteriores = itemsPubCanillaData + .Where(i => i.Item.IdCanilla == itemExistente.IdCanilla && + i.Item.IdPorcMon != idPorcMon && + i.Item.VigenciaD.Date <= updateDto.VigenciaH.Value.Date && + i.Item.VigenciaD.Date > itemExistente.VigenciaD.Date); + if(itemsPosteriores.Any()) + { + return (false, "No se puede cerrar este período porque existen configuraciones posteriores para este canillita que se solaparían."); + } + } + + itemExistente.PorcMon = updateDto.PorcMon; + itemExistente.EsPorcentaje = updateDto.EsPorcentaje; + if (updateDto.VigenciaH.HasValue) + { + itemExistente.VigenciaH = updateDto.VigenciaH.Value.Date; + } + + var actualizado = await _porcMonCanillaRepository.UpdateAsync(itemExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar registro."); + + transaction.Commit(); + _logger.LogInformation("PorcMonCanilla ID {Id} actualizado por Usuario ID {UserId}.", idPorcMon, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarAsync PorcMonCanilla ID: {Id}", idPorcMon); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idPorcMon, 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(); + try + { + var itemAEliminar = await _porcMonCanillaRepository.GetByIdAsync(idPorcMon); + if (itemAEliminar == null) return (false, "Registro no encontrado."); + + if (itemAEliminar.VigenciaH == null) + { + var todosItemsPubCanillaData = await _porcMonCanillaRepository.GetByPublicacionIdAsync(itemAEliminar.IdPublicacion); + var todosItemsPubCanilla = todosItemsPubCanillaData + .Where(i => i.Item.IdCanilla == itemAEliminar.IdCanilla) + .Select(i => i.Item) + .OrderByDescending(i => i.VigenciaD).ToList(); + + var indiceActual = todosItemsPubCanilla.FindIndex(i => i.IdPorcMon == idPorcMon); + if(indiceActual != -1 && (indiceActual + 1) < todosItemsPubCanilla.Count) + { + var itemAnteriorDirecto = todosItemsPubCanilla[indiceActual + 1]; + if(itemAnteriorDirecto.VigenciaH.HasValue && + itemAnteriorDirecto.VigenciaH.Value.Date == itemAEliminar.VigenciaD.AddDays(-1).Date) + { + itemAnteriorDirecto.VigenciaH = null; + await _porcMonCanillaRepository.UpdateAsync(itemAnteriorDirecto, idUsuario, transaction); + } + } + } + + var eliminado = await _porcMonCanillaRepository.DeleteAsync(idPorcMon, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar registro."); + + transaction.Commit(); + _logger.LogInformation("PorcMonCanilla ID {Id} eliminado por Usuario ID {UserId}.", idPorcMon, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarAsync PorcMonCanilla ID: {Id}", idPorcMon); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/PorcPagoService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/PorcPagoService.cs new file mode 100644 index 0000000..4a8ced5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/PorcPagoService.cs @@ -0,0 +1,234 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class PorcPagoService : IPorcPagoService + { + private readonly IPorcPagoRepository _porcPagoRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly IDistribuidorRepository _distribuidorRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PorcPagoService( + IPorcPagoRepository porcPagoRepository, + IPublicacionRepository publicacionRepository, + IDistribuidorRepository distribuidorRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _porcPagoRepository = porcPagoRepository; + _publicacionRepository = publicacionRepository; + _distribuidorRepository = distribuidorRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private PorcPagoDto MapToDto((PorcPago PorcPago, string NombreDistribuidor) data) => new PorcPagoDto + { + IdPorcentaje = data.PorcPago.IdPorcentaje, + IdPublicacion = data.PorcPago.IdPublicacion, + IdDistribuidor = data.PorcPago.IdDistribuidor, + NombreDistribuidor = data.NombreDistribuidor, + VigenciaD = data.PorcPago.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = data.PorcPago.VigenciaH?.ToString("yyyy-MM-dd"), + Porcentaje = data.PorcPago.Porcentaje + }; + + private async Task MapToDtoWithLookup(PorcPago porcPago) + { + var distribuidorData = await _distribuidorRepository.GetByIdAsync(porcPago.IdDistribuidor); + return new PorcPagoDto + { + IdPorcentaje = porcPago.IdPorcentaje, + IdPublicacion = porcPago.IdPublicacion, + IdDistribuidor = porcPago.IdDistribuidor, + NombreDistribuidor = distribuidorData.Distribuidor?.Nombre ?? "Distribuidor Desconocido", + VigenciaD = porcPago.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = porcPago.VigenciaH?.ToString("yyyy-MM-dd"), + Porcentaje = porcPago.Porcentaje + }; + } + + public async Task> ObtenerPorPublicacionIdAsync(int idPublicacion) + { + var data = await _porcPagoRepository.GetByPublicacionIdAsync(idPublicacion); + return data.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int idPorcentaje) + { + var porcPago = await _porcPagoRepository.GetByIdAsync(idPorcentaje); + if (porcPago == null) return null; + return await MapToDtoWithLookup(porcPago); + } + + public async Task<(PorcPagoDto? PorcPago, string? Error)> CrearAsync(CreatePorcPagoDto createDto, int idUsuario) + { + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación especificada no existe."); + var distribuidorData = await _distribuidorRepository.GetByIdAsync(createDto.IdDistribuidor); + if (distribuidorData.Distribuidor == null) + return (null, "El distribuidor especificado no existe."); + + var porcPagoActivo = await _porcPagoRepository.GetActiveByPublicacionDistribuidorAndDateAsync(createDto.IdPublicacion, createDto.IdDistribuidor, createDto.VigenciaD.Date); + if (porcPagoActivo != null) + { + return (null, $"Ya existe un porcentaje de pago activo para esta publicación y distribuidor en la fecha {createDto.VigenciaD:dd/MM/yyyy}. Cierre el período anterior primero."); + } + + var nuevoPorcPago = new PorcPago + { + IdPublicacion = createDto.IdPublicacion, + IdDistribuidor = createDto.IdDistribuidor, + VigenciaD = createDto.VigenciaD.Date, + VigenciaH = null, + Porcentaje = createDto.Porcentaje + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var porcPagoAnterior = await _porcPagoRepository.GetPreviousActivePorcPagoAsync(createDto.IdPublicacion, createDto.IdDistribuidor, nuevoPorcPago.VigenciaD, transaction); + if (porcPagoAnterior != null) + { + if (porcPagoAnterior.VigenciaD.Date >= nuevoPorcPago.VigenciaD.Date) + { + transaction.Rollback(); + return (null, $"La fecha de inicio del nuevo porcentaje ({nuevoPorcPago.VigenciaD:dd/MM/yyyy}) no puede ser anterior o igual a la del último porcentaje vigente ({porcPagoAnterior.VigenciaD:dd/MM/yyyy}) para este distribuidor."); + } + porcPagoAnterior.VigenciaH = nuevoPorcPago.VigenciaD.AddDays(-1); + await _porcPagoRepository.UpdateAsync(porcPagoAnterior, idUsuario, transaction); + } + + var porcPagoCreado = await _porcPagoRepository.CreateAsync(nuevoPorcPago, idUsuario, transaction); + if (porcPagoCreado == null) throw new DataException("Error al crear el porcentaje de pago."); + + transaction.Commit(); + _logger.LogInformation("PorcPago ID {Id} creado por Usuario ID {UserId}.", porcPagoCreado.IdPorcentaje, idUsuario); + return (new PorcPagoDto { // Construir DTO manualmente ya que tenemos NombreDistribuidor + IdPorcentaje = porcPagoCreado.IdPorcentaje, + IdPublicacion = porcPagoCreado.IdPublicacion, + IdDistribuidor = porcPagoCreado.IdDistribuidor, + NombreDistribuidor = distribuidorData.Distribuidor.Nombre, + VigenciaD = porcPagoCreado.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = porcPagoCreado.VigenciaH?.ToString("yyyy-MM-dd"), + Porcentaje = porcPagoCreado.Porcentaje + }, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync PorcPago para Pub ID {IdPub}, Dist ID {IdDist}", createDto.IdPublicacion, createDto.IdDistribuidor); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPorcentaje, UpdatePorcPagoDto 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(); + try + { + var porcPagoExistente = await _porcPagoRepository.GetByIdAsync(idPorcentaje); + if (porcPagoExistente == null) return (false, "Porcentaje de pago no encontrado."); + + if (updateDto.VigenciaH.HasValue && updateDto.VigenciaH.Value.Date < porcPagoExistente.VigenciaD.Date) + return (false, "Vigencia Hasta no puede ser anterior a Vigencia Desde."); + + if (updateDto.VigenciaH.HasValue) + { + var porcentajesPubDist = await _porcPagoRepository.GetByPublicacionIdAsync(porcPagoExistente.IdPublicacion); + var porcentajesPosteriores = porcentajesPubDist + .Where(p => p.PorcPago.IdDistribuidor == porcPagoExistente.IdDistribuidor && + p.PorcPago.IdPorcentaje != idPorcentaje && + p.PorcPago.VigenciaD.Date <= updateDto.VigenciaH.Value.Date && + p.PorcPago.VigenciaD.Date > porcPagoExistente.VigenciaD.Date); + if(porcentajesPosteriores.Any()) + { + return (false, "No se puede cerrar este período porque existen porcentajes posteriores para este distribuidor que se solaparían."); + } + } + + porcPagoExistente.Porcentaje = updateDto.Porcentaje; + if (updateDto.VigenciaH.HasValue) + { + porcPagoExistente.VigenciaH = updateDto.VigenciaH.Value.Date; + } + + var actualizado = await _porcPagoRepository.UpdateAsync(porcPagoExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar porcentaje de pago."); + + transaction.Commit(); + _logger.LogInformation("PorcPago ID {Id} actualizado por Usuario ID {UserId}.", idPorcentaje, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Porcentaje no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarAsync PorcPago ID: {Id}", idPorcentaje); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idPorcentaje, 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(); + try + { + var porcPagoAEliminar = await _porcPagoRepository.GetByIdAsync(idPorcentaje); + if (porcPagoAEliminar == null) return (false, "Porcentaje de pago no encontrado."); + + if (porcPagoAEliminar.VigenciaH == null) + { + var todosPorcPubDistData = await _porcPagoRepository.GetByPublicacionIdAsync(porcPagoAEliminar.IdPublicacion); + var todosPorcPubDist = todosPorcPubDistData + .Where(p => p.PorcPago.IdDistribuidor == porcPagoAEliminar.IdDistribuidor) + .Select(p => p.PorcPago) + .OrderByDescending(p => p.VigenciaD).ToList(); + + var indiceActual = todosPorcPubDist.FindIndex(p => p.IdPorcentaje == idPorcentaje); + if(indiceActual != -1 && (indiceActual + 1) < todosPorcPubDist.Count) + { + var porcPagoAnteriorDirecto = todosPorcPubDist[indiceActual + 1]; + if(porcPagoAnteriorDirecto.VigenciaH.HasValue && + porcPagoAnteriorDirecto.VigenciaH.Value.Date == porcPagoAEliminar.VigenciaD.AddDays(-1).Date) + { + porcPagoAnteriorDirecto.VigenciaH = null; + await _porcPagoRepository.UpdateAsync(porcPagoAnteriorDirecto, idUsuario, transaction); + } + } + } + + var eliminado = await _porcPagoRepository.DeleteAsync(idPorcentaje, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar porcentaje de pago."); + + transaction.Commit(); + _logger.LogInformation("PorcPago ID {Id} eliminado por Usuario ID {UserId}.", idPorcentaje, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Porcentaje no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarAsync PorcPago ID: {Id}", idPorcentaje); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/PubliSeccionService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/PubliSeccionService.cs new file mode 100644 index 0000000..9506d8b --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/PubliSeccionService.cs @@ -0,0 +1,153 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class PubliSeccionService : IPubliSeccionService + { + private readonly IPubliSeccionRepository _publiSeccionRepository; + private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PubliSeccionService( + IPubliSeccionRepository publiSeccionRepository, + IPublicacionRepository publicacionRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _publiSeccionRepository = publiSeccionRepository; + _publicacionRepository = publicacionRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private PubliSeccionDto MapToDto(PubliSeccion seccion) => new PubliSeccionDto + { + IdSeccion = seccion.IdSeccion, + IdPublicacion = seccion.IdPublicacion, + Nombre = seccion.Nombre, + Estado = seccion.Estado + }; + + public async Task> ObtenerPorPublicacionIdAsync(int idPublicacion, bool? soloActivas = null) + { + var secciones = await _publiSeccionRepository.GetByPublicacionIdAsync(idPublicacion, soloActivas); + return secciones.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int idSeccion) + { + var seccion = await _publiSeccionRepository.GetByIdAsync(idSeccion); + return seccion == null ? null : MapToDto(seccion); + } + + public async Task<(PubliSeccionDto? Seccion, string? Error)> CrearAsync(CreatePubliSeccionDto createDto, int idUsuario) + { + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación especificada no existe."); + if (await _publiSeccionRepository.ExistsByNameInPublicacionAsync(createDto.Nombre, createDto.IdPublicacion)) + return (null, "Ya existe una sección con ese nombre para esta publicación."); + + var nuevaSeccion = new PubliSeccion + { + IdPublicacion = createDto.IdPublicacion, + Nombre = createDto.Nombre, + Estado = createDto.Estado + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var seccionCreada = await _publiSeccionRepository.CreateAsync(nuevaSeccion, idUsuario, transaction); + if (seccionCreada == null) throw new DataException("Error al crear la sección."); + + transaction.Commit(); + _logger.LogInformation("Sección ID {Id} creada por Usuario ID {UserId}.", seccionCreada.IdSeccion, idUsuario); + return (MapToDto(seccionCreada), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync PubliSeccion para Pub ID {IdPub}, Nombre: {Nombre}", createDto.IdPublicacion, createDto.Nombre); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idSeccion, UpdatePubliSeccionDto 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(); + try + { + var seccionExistente = await _publiSeccionRepository.GetByIdAsync(idSeccion); // Obtener dentro de TX + if (seccionExistente == null) return (false, "Sección no encontrada."); + + // Validar unicidad de nombre solo si el nombre ha cambiado + if (seccionExistente.Nombre != updateDto.Nombre && + await _publiSeccionRepository.ExistsByNameInPublicacionAsync(updateDto.Nombre, seccionExistente.IdPublicacion, idSeccion)) + { + return (false, "Ya existe otra sección con ese nombre para esta publicación."); + } + + seccionExistente.Nombre = updateDto.Nombre; + seccionExistente.Estado = updateDto.Estado; + + var actualizado = await _publiSeccionRepository.UpdateAsync(seccionExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar la sección."); + + transaction.Commit(); + _logger.LogInformation("Sección ID {Id} actualizada por Usuario ID {UserId}.", idSeccion, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Sección no encontrada."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarAsync PubliSeccion ID: {Id}", idSeccion); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idSeccion, 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(); + try + { + var seccionExistente = await _publiSeccionRepository.GetByIdAsync(idSeccion); // Obtener dentro de TX + if (seccionExistente == null) return (false, "Sección no encontrada."); + + if (await _publiSeccionRepository.IsInUseAsync(idSeccion)) + { + return (false, "No se puede eliminar. La sección está siendo utilizada en registros de tiradas o stock de bobinas."); + } + + var eliminado = await _publiSeccionRepository.DeleteAsync(idSeccion, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar la sección."); + + transaction.Commit(); + _logger.LogInformation("Sección ID {Id} eliminada por Usuario ID {UserId}.", idSeccion, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Sección no encontrada."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarAsync PubliSeccion ID: {Id}", idSeccion); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/RecargoZonaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/RecargoZonaService.cs new file mode 100644 index 0000000..5d8b792 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/RecargoZonaService.cs @@ -0,0 +1,247 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class RecargoZonaService : IRecargoZonaService + { + private readonly IRecargoZonaRepository _recargoZonaRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly IZonaRepository _zonaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public RecargoZonaService( + IRecargoZonaRepository recargoZonaRepository, + IPublicacionRepository publicacionRepository, + IZonaRepository zonaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _recargoZonaRepository = recargoZonaRepository; + _publicacionRepository = publicacionRepository; + _zonaRepository = zonaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private RecargoZonaDto MapToDto((RecargoZona Recargo, string NombreZona) data) => new RecargoZonaDto + { + IdRecargo = data.Recargo.IdRecargo, + IdPublicacion = data.Recargo.IdPublicacion, + IdZona = data.Recargo.IdZona, + NombreZona = data.NombreZona, + VigenciaD = data.Recargo.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = data.Recargo.VigenciaH?.ToString("yyyy-MM-dd"), + Valor = data.Recargo.Valor + }; + + // Helper para mapear cuando solo tenemos el RecargoZona y necesitamos buscar NombreZona + private async Task MapToDtoWithZonaLookup(RecargoZona recargo) + { + var zona = await _zonaRepository.GetByIdAsync(recargo.IdZona); // zonaRepository.GetByIdAsync devuelve ZonaDto + return new RecargoZonaDto + { + IdRecargo = recargo.IdRecargo, + IdPublicacion = recargo.IdPublicacion, + IdZona = recargo.IdZona, + NombreZona = zona?.Nombre ?? "Zona Desconocida/Inactiva", // Usar la propiedad Nombre del ZonaDto + VigenciaD = recargo.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = recargo.VigenciaH?.ToString("yyyy-MM-dd"), + Valor = recargo.Valor + }; + } + + + public async Task> ObtenerPorPublicacionIdAsync(int idPublicacion) + { + var recargosData = await _recargoZonaRepository.GetByPublicacionIdAsync(idPublicacion); + return recargosData + .Select(rd => MapToDto(rd)) // MapToDto ahora devuelve RecargoZonaDto? + .Where(dto => dto != null) + .Select(dto => dto!); // Cast no nulo + } + + public async Task ObtenerPorIdAsync(int idRecargo) + { + var recargo = await _recargoZonaRepository.GetByIdAsync(idRecargo); + if (recargo == null) return null; + + var zona = await _zonaRepository.GetByIdAsync(recargo.IdZona); // Obtiene ZonaDto + return new RecargoZonaDto { + IdRecargo = recargo.IdRecargo, + IdPublicacion = recargo.IdPublicacion, + IdZona = recargo.IdZona, + NombreZona = zona?.Nombre ?? "Zona Desconocida/Inactiva", + VigenciaD = recargo.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = recargo.VigenciaH?.ToString("yyyy-MM-dd"), + Valor = recargo.Valor + }; + } + + public async Task<(RecargoZonaDto? Recargo, string? Error)> CrearAsync(CreateRecargoZonaDto createDto, int idUsuario) + { + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación especificada no existe."); + var zona = await _zonaRepository.GetByIdAsync(createDto.IdZona); // Devuelve ZonaDto + if (zona == null) + return (null, "La zona especificada no existe o no está activa."); + + // Usar createDto.VigenciaD directamente que ya es DateTime + var recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(createDto.IdPublicacion, createDto.IdZona, createDto.VigenciaD.Date); + if (recargoActivo != null) + { + return (null, $"Ya existe un recargo activo para esta publicación y zona en la fecha {createDto.VigenciaD:dd/MM/yyyy}. Primero debe cerrar el período anterior."); + } + + var nuevoRecargo = new RecargoZona + { + IdPublicacion = createDto.IdPublicacion, + IdZona = createDto.IdZona, + VigenciaD = createDto.VigenciaD.Date, + VigenciaH = null, + Valor = createDto.Valor + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var recargoAnterior = await _recargoZonaRepository.GetPreviousActiveRecargoAsync(createDto.IdPublicacion, createDto.IdZona, nuevoRecargo.VigenciaD, transaction); + if (recargoAnterior != null) + { + if (recargoAnterior.VigenciaD.Date >= nuevoRecargo.VigenciaD.Date) // Comparar solo fechas + { + transaction.Rollback(); + return (null, $"La fecha de inicio del nuevo recargo ({nuevoRecargo.VigenciaD:dd/MM/yyyy}) no puede ser anterior o igual a la del último recargo vigente ({recargoAnterior.VigenciaD:dd/MM/yyyy}) para esta zona."); + } + recargoAnterior.VigenciaH = nuevoRecargo.VigenciaD.AddDays(-1); + await _recargoZonaRepository.UpdateAsync(recargoAnterior, idUsuario, transaction); + } + + var recargoCreado = await _recargoZonaRepository.CreateAsync(nuevoRecargo, idUsuario, transaction); + if (recargoCreado == null) throw new DataException("Error al crear el recargo."); + + transaction.Commit(); + _logger.LogInformation("Recargo ID {Id} creado por Usuario ID {UserId}.", recargoCreado.IdRecargo, idUsuario); + // Pasar el nombre de la zona ya obtenido + return (new RecargoZonaDto { + IdRecargo = recargoCreado.IdRecargo, IdPublicacion = recargoCreado.IdPublicacion, IdZona = recargoCreado.IdZona, + NombreZona = zona.Nombre, VigenciaD = recargoCreado.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = recargoCreado.VigenciaH?.ToString("yyyy-MM-dd"), Valor = recargoCreado.Valor + }, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync RecargoZona para Pub ID {IdPub}, Zona ID {IdZona}", createDto.IdPublicacion, createDto.IdZona); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idRecargo, UpdateRecargoZonaDto 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(); + try + { + var recargoExistente = await _recargoZonaRepository.GetByIdAsync(idRecargo); + if (recargoExistente == null) return (false, "Recargo por zona no encontrado."); + + if (updateDto.VigenciaH.HasValue && updateDto.VigenciaH.Value.Date < recargoExistente.VigenciaD.Date) + return (false, "Vigencia Hasta no puede ser anterior a Vigencia Desde."); + + if (updateDto.VigenciaH.HasValue) + { + var recargosDeLaPublicacion = await _recargoZonaRepository.GetByPublicacionIdAsync(recargoExistente.IdPublicacion); + var recargosPosteriores = recargosDeLaPublicacion + .Where(r => r.Recargo.IdZona == recargoExistente.IdZona && + r.Recargo.IdRecargo != idRecargo && + r.Recargo.VigenciaD.Date <= updateDto.VigenciaH.Value.Date && + r.Recargo.VigenciaD.Date > recargoExistente.VigenciaD.Date); + if(recargosPosteriores.Any()) + { + return (false, "No se puede cerrar este período porque existen recargos posteriores para esta zona que se solaparían."); + } + } + + recargoExistente.Valor = updateDto.Valor; + if (updateDto.VigenciaH.HasValue) + { + recargoExistente.VigenciaH = updateDto.VigenciaH.Value.Date; + } + + var actualizado = await _recargoZonaRepository.UpdateAsync(recargoExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar recargo."); + + transaction.Commit(); + _logger.LogInformation("Recargo ID {Id} actualizado por Usuario ID {UserId}.", idRecargo, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Recargo no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarAsync RecargoZona ID: {Id}", idRecargo); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idRecargo, 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(); + try + { + var recargoAEliminar = await _recargoZonaRepository.GetByIdAsync(idRecargo); + if (recargoAEliminar == null) return (false, "Recargo no encontrado."); + + if (recargoAEliminar.VigenciaH == null) + { + var todosRecargosPubZonaData = await _recargoZonaRepository.GetByPublicacionIdAsync(recargoAEliminar.IdPublicacion); + var todosRecargosPubZona = todosRecargosPubZonaData + .Where(r => r.Recargo.IdZona == recargoAEliminar.IdZona) + .Select(r => r.Recargo) + .OrderByDescending(r => r.VigenciaD).ToList(); + + var indiceActual = todosRecargosPubZona.FindIndex(r => r.IdRecargo == idRecargo); + if(indiceActual != -1 && (indiceActual + 1) < todosRecargosPubZona.Count) + { + var recargoAnteriorDirecto = todosRecargosPubZona[indiceActual + 1]; + if(recargoAnteriorDirecto.VigenciaH.HasValue && recargoAnteriorDirecto.VigenciaH.Value.Date == recargoAEliminar.VigenciaD.AddDays(-1).Date) + { + recargoAnteriorDirecto.VigenciaH = null; + await _recargoZonaRepository.UpdateAsync(recargoAnteriorDirecto, idUsuario, transaction); + _logger.LogInformation("Recargo anterior ID {IdRecargoAnterior} reabierto tras eliminación de Recargo ID {IdRecargoEliminado}.", recargoAnteriorDirecto.IdRecargo, idRecargo); + } + } + } + + var eliminado = await _recargoZonaRepository.DeleteAsync(idRecargo, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar recargo."); + + transaction.Commit(); + _logger.LogInformation("Recargo ID {Id} eliminado por Usuario ID {UserId}.", idRecargo, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Recargo no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarAsync RecargoZona ID: {Id}", idRecargo); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/SalidaOtroDestinoService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/SalidaOtroDestinoService.cs new file mode 100644 index 0000000..738b0c1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/SalidaOtroDestinoService.cs @@ -0,0 +1,165 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class SalidaOtroDestinoService : ISalidaOtroDestinoService + { + private readonly ISalidaOtroDestinoRepository _salidaRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly IOtroDestinoRepository _otroDestinoRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public SalidaOtroDestinoService( + ISalidaOtroDestinoRepository salidaRepository, + IPublicacionRepository publicacionRepository, + IOtroDestinoRepository otroDestinoRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _salidaRepository = salidaRepository; + _publicacionRepository = publicacionRepository; + _otroDestinoRepository = otroDestinoRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task MapToDto(SalidaOtroDestino salida) + { + if (salida == null) return null!; // Debería ser manejado por el llamador + + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(salida.IdPublicacion); + var destino = await _otroDestinoRepository.GetByIdAsync(salida.IdDestino); + + return new SalidaOtroDestinoDto + { + IdParte = salida.IdParte, + IdPublicacion = salida.IdPublicacion, + NombrePublicacion = publicacion?.Nombre ?? "Publicación Desconocida", + IdDestino = salida.IdDestino, + NombreDestino = destino?.Nombre ?? "Destino Desconocido", + Fecha = salida.Fecha.ToString("yyyy-MM-dd"), + Cantidad = salida.Cantidad, + Observacion = salida.Observacion + }; + } + + public async Task> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino) + { + var salidas = await _salidaRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idDestino); + var dtos = new List(); + foreach (var salida in salidas) + { + dtos.Add(await MapToDto(salida)); + } + return dtos; + } + + public async Task ObtenerPorIdAsync(int idParte) + { + var salida = await _salidaRepository.GetByIdAsync(idParte); + return salida == null ? null : await MapToDto(salida); + } + + public async Task<(SalidaOtroDestinoDto? Salida, string? Error)> CrearAsync(CreateSalidaOtroDestinoDto createDto, int idUsuario) + { + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación especificada no existe o no está habilitada."); + if (await _otroDestinoRepository.GetByIdAsync(createDto.IdDestino) == null) + return (null, "El destino especificado no existe."); + + var nuevaSalida = new SalidaOtroDestino + { + IdPublicacion = createDto.IdPublicacion, + IdDestino = createDto.IdDestino, + Fecha = createDto.Fecha.Date, + Cantidad = createDto.Cantidad, + Observacion = createDto.Observacion + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var salidaCreada = await _salidaRepository.CreateAsync(nuevaSalida, idUsuario, transaction); + if (salidaCreada == null) throw new DataException("Error al registrar la salida."); + + transaction.Commit(); + _logger.LogInformation("SalidaOtroDestino ID {Id} creada por Usuario ID {UserId}.", salidaCreada.IdParte, idUsuario); + return (await MapToDto(salidaCreada), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync SalidaOtroDestino para Pub ID {IdPub}", createDto.IdPublicacion); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idParte, UpdateSalidaOtroDestinoDto 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(); + try + { + var salidaExistente = await _salidaRepository.GetByIdAsync(idParte); // Obtener dentro de TX + if (salidaExistente == null) return (false, "Registro de salida no encontrado."); + + // Actualizar solo los campos permitidos + salidaExistente.Cantidad = updateDto.Cantidad; + salidaExistente.Observacion = updateDto.Observacion; + + var actualizado = await _salidaRepository.UpdateAsync(salidaExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar la salida."); + + transaction.Commit(); + _logger.LogInformation("SalidaOtroDestino ID {Id} actualizada por Usuario ID {UserId}.", idParte, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarAsync SalidaOtroDestino ID: {Id}", idParte); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idParte, 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(); + try + { + var salidaExistente = await _salidaRepository.GetByIdAsync(idParte); + if (salidaExistente == null) return (false, "Registro de salida no encontrado."); + + var eliminado = await _salidaRepository.DeleteAsync(idParte, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar la salida."); + + transaction.Commit(); + _logger.LogInformation("SalidaOtroDestino ID {Id} eliminada por Usuario ID {UserId}.", idParte, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarAsync SalidaOtroDestino ID: {Id}", idParte); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs new file mode 100644 index 0000000..1f2917e --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/IStockBobinaService.cs @@ -0,0 +1,20 @@ +using GestionIntegral.Api.Dtos.Impresion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public interface IStockBobinaService + { + Task> ObtenerTodosAsync( + int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, + int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta); + + Task ObtenerPorIdAsync(int idBobina); + Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarDatosBobinaDisponibleAsync(int idBobina, UpdateStockBobinaDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> CambiarEstadoBobinaAsync(int idBobina, CambiarEstadoBobinaDto cambiarEstadoDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarIngresoErroneoAsync(int idBobina, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/ITiradaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/ITiradaService.cs new file mode 100644 index 0000000..b8fdf2d --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/ITiradaService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Impresion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public interface ITiradaService + { + Task> ObtenerTiradasAsync(DateTime? fecha, int? idPublicacion, int? idPlanta); + // GetById podría ser útil si se editan tiradas individuales, pero la creación es el foco principal. + // Task ObtenerTiradaPorIdRegistroAsync(int idRegistroTirada); // Para bob_RegTiradas + Task<(TiradaDto? TiradaCreada, string? Error)> RegistrarTiradaCompletaAsync(CreateTiradaRequestDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarTiradaCompletaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs new file mode 100644 index 0000000..5bb808c --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/StockBobinaService.cs @@ -0,0 +1,278 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Models.Impresion; +using GestionIntegral.Api.Models.Distribucion; // Para Publicacion, PubliSeccion +using GestionIntegral.Api.Data.Repositories.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public class StockBobinaService : IStockBobinaService + { + private readonly IStockBobinaRepository _stockBobinaRepository; + private readonly ITipoBobinaRepository _tipoBobinaRepository; + private readonly IPlantaRepository _plantaRepository; + private readonly IEstadoBobinaRepository _estadoBobinaRepository; + private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion + private readonly IPubliSeccionRepository _publiSeccionRepository; // Para validar IdSeccion + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public StockBobinaService( + IStockBobinaRepository stockBobinaRepository, + ITipoBobinaRepository tipoBobinaRepository, + IPlantaRepository plantaRepository, + IEstadoBobinaRepository estadoBobinaRepository, + IPublicacionRepository publicacionRepository, + IPubliSeccionRepository publiSeccionRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _stockBobinaRepository = stockBobinaRepository; + _tipoBobinaRepository = tipoBobinaRepository; + _plantaRepository = plantaRepository; + _estadoBobinaRepository = estadoBobinaRepository; + _publicacionRepository = publicacionRepository; + _publiSeccionRepository = publiSeccionRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + // Mapeo complejo porque necesitamos nombres de entidades relacionadas + private async Task MapToDto(StockBobina bobina) + { + if (bobina == null) return null!; // Debería ser manejado por el llamador + + var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(bobina.IdTipoBobina); + var planta = await _plantaRepository.GetByIdAsync(bobina.IdPlanta); + var estado = await _estadoBobinaRepository.GetByIdAsync(bobina.IdEstadoBobina); + Publicacion? publicacion = null; + PubliSeccion? seccion = null; + + if (bobina.IdPublicacion.HasValue) + publicacion = await _publicacionRepository.GetByIdSimpleAsync(bobina.IdPublicacion.Value); + if (bobina.IdSeccion.HasValue) + seccion = await _publiSeccionRepository.GetByIdAsync(bobina.IdSeccion.Value); // Asume que GetByIdAsync existe + + return new StockBobinaDto + { + IdBobina = bobina.IdBobina, + IdTipoBobina = bobina.IdTipoBobina, + NombreTipoBobina = tipoBobina?.Denominacion ?? "N/A", + NroBobina = bobina.NroBobina, + Peso = bobina.Peso, + IdPlanta = bobina.IdPlanta, + NombrePlanta = planta?.Nombre ?? "N/A", + IdEstadoBobina = bobina.IdEstadoBobina, + NombreEstadoBobina = estado?.Denominacion ?? "N/A", + Remito = bobina.Remito, + FechaRemito = bobina.FechaRemito.ToString("yyyy-MM-dd"), + FechaEstado = bobina.FechaEstado?.ToString("yyyy-MM-dd"), + IdPublicacion = bobina.IdPublicacion, + NombrePublicacion = publicacion?.Nombre, + IdSeccion = bobina.IdSeccion, + NombreSeccion = seccion?.Nombre, + Obs = bobina.Obs + }; + } + + public async Task> ObtenerTodosAsync( + int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, + int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta) + { + var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta); + var dtos = new List(); + foreach (var bobina in bobinas) + { + dtos.Add(await MapToDto(bobina)); + } + return dtos; + } + + public async Task ObtenerPorIdAsync(int idBobina) + { + var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); + return bobina == null ? null : await MapToDto(bobina); + } + + public async Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario) + { + if (await _tipoBobinaRepository.GetByIdAsync(createDto.IdTipoBobina) == null) + return (null, "Tipo de bobina inválido."); + if (await _plantaRepository.GetByIdAsync(createDto.IdPlanta) == null) + return (null, "Planta inválida."); + if (await _stockBobinaRepository.GetByNroBobinaAsync(createDto.NroBobina) != null) + return (null, $"El número de bobina '{createDto.NroBobina}' ya existe."); + + var nuevaBobina = new StockBobina + { + IdTipoBobina = createDto.IdTipoBobina, + NroBobina = createDto.NroBobina, + Peso = createDto.Peso, + IdPlanta = createDto.IdPlanta, + IdEstadoBobina = 1, // 1 = Disponible (según contexto previo) + Remito = createDto.Remito, + FechaRemito = createDto.FechaRemito.Date, + FechaEstado = createDto.FechaRemito.Date, // Estado inicial en fecha de remito + IdPublicacion = null, + IdSeccion = null, + Obs = null + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction); + if (bobinaCreada == null) throw new DataException("Error al ingresar la bobina."); + transaction.Commit(); + _logger.LogInformation("Bobina ID {Id} ingresada por Usuario ID {UserId}.", bobinaCreada.IdBobina, idUsuario); + return (await MapToDto(bobinaCreada), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error IngresarBobinaAsync: {NroBobina}", createDto.NroBobina); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarDatosBobinaDisponibleAsync(int idBobina, UpdateStockBobinaDto 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(); + try + { + var bobinaExistente = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de TX + if (bobinaExistente == null) return (false, "Bobina no encontrada."); + if (bobinaExistente.IdEstadoBobina != 1) // Solo se pueden editar datos si está "Disponible" + return (false, "Solo se pueden modificar los datos de una bobina en estado 'Disponible'."); + + // Validar unicidad de NroBobina si cambió + if (bobinaExistente.NroBobina != updateDto.NroBobina && + await _stockBobinaRepository.GetByNroBobinaAsync(updateDto.NroBobina) != null) // Validar fuera de TX o pasar TX al repo + { + try { transaction.Rollback(); } catch {} // Rollback antes de retornar por validación + return (false, $"El nuevo número de bobina '{updateDto.NroBobina}' ya existe."); + } + if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null) + return (false, "Tipo de bobina inválido."); + if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null) + return (false, "Planta inválida."); + + + bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina; + bobinaExistente.NroBobina = updateDto.NroBobina; + bobinaExistente.Peso = updateDto.Peso; + bobinaExistente.IdPlanta = updateDto.IdPlanta; + bobinaExistente.Remito = updateDto.Remito; + bobinaExistente.FechaRemito = updateDto.FechaRemito.Date; + // FechaEstado se mantiene ya que el estado no cambia aquí + + var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados"); + if (!actualizado) throw new DataException("Error al actualizar la bobina."); + transaction.Commit(); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarDatosBobinaDisponibleAsync ID: {IdBobina}", idBobina); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> CambiarEstadoBobinaAsync(int idBobina, CambiarEstadoBobinaDto cambiarEstadoDto, 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(); + try + { + var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); + if (bobina == null) return (false, "Bobina no encontrada."); + + var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId); + if (nuevoEstado == null) return (false, "El nuevo estado especificado no es válido."); + + // Validaciones de flujo de estados + if (bobina.IdEstadoBobina == cambiarEstadoDto.NuevoEstadoId) + return (false, "La bobina ya se encuentra en ese estado."); + if (bobina.IdEstadoBobina == 3 && cambiarEstadoDto.NuevoEstadoId != 3) // 3 = Dañada + return (false, "Una bobina dañada no puede cambiar de estado."); + if (bobina.IdEstadoBobina == 2 && cambiarEstadoDto.NuevoEstadoId == 1) // 2 = En Uso, 1 = Disponible + return (false, "Una bobina 'En Uso' no puede volver a 'Disponible' directamente mediante esta acción."); + + + bobina.IdEstadoBobina = cambiarEstadoDto.NuevoEstadoId; + bobina.FechaEstado = cambiarEstadoDto.FechaCambioEstado.Date; + bobina.Obs = cambiarEstadoDto.Obs ?? bobina.Obs; // Mantener obs si no se provee uno nuevo + + if (cambiarEstadoDto.NuevoEstadoId == 2) // "En Uso" + { + if (!cambiarEstadoDto.IdPublicacion.HasValue || !cambiarEstadoDto.IdSeccion.HasValue) + return (false, "Para el estado 'En Uso', se requiere Publicación y Sección."); + if (await _publicacionRepository.GetByIdSimpleAsync(cambiarEstadoDto.IdPublicacion.Value) == null) + return (false, "Publicación inválida."); + if (await _publiSeccionRepository.GetByIdAsync(cambiarEstadoDto.IdSeccion.Value) == null) // Asume GetByIdAsync en IPubliSeccionRepository + return (false, "Sección inválida."); + + bobina.IdPublicacion = cambiarEstadoDto.IdPublicacion.Value; + bobina.IdSeccion = cambiarEstadoDto.IdSeccion.Value; + } + else + { // Si no es "En Uso", limpiar estos campos + bobina.IdPublicacion = null; + bobina.IdSeccion = null; + } + + var actualizado = await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, $"Estado Cambiado a: {nuevoEstado.Denominacion}"); + if (!actualizado) throw new DataException("Error al cambiar estado de la bobina."); + transaction.Commit(); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CambiarEstadoBobinaAsync ID: {IdBobina}", idBobina); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarIngresoErroneoAsync(int idBobina, 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(); + try + { + var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); + if (bobina == null) return (false, "Bobina no encontrada."); + if (bobina.IdEstadoBobina != 1) // Solo se pueden eliminar las "Disponibles" (ingresos erróneos) + return (false, "Solo se pueden eliminar ingresos de bobinas que estén en estado 'Disponible'."); + + var eliminado = await _stockBobinaRepository.DeleteAsync(idBobina, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar la bobina."); + transaction.Commit(); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarIngresoErroneoAsync ID: {IdBobina}", idBobina); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/TiradaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/TiradaService.cs new file mode 100644 index 0000000..4ed5d47 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/TiradaService.cs @@ -0,0 +1,224 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Models.Distribucion; // Para Publicacion, PubliSeccion +using GestionIntegral.Api.Models.Impresion; // Para RegTirada, RegPublicacionSeccion +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public class TiradaService : ITiradaService + { + private readonly IRegTiradaRepository _regTiradaRepository; + private readonly IRegPublicacionSeccionRepository _regPublicacionSeccionRepository; + private readonly IPublicacionRepository _publicacionRepository; + private readonly IPlantaRepository _plantaRepository; + private readonly IPubliSeccionRepository _publiSeccionRepository; // Para validar IDs de sección + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public TiradaService( + IRegTiradaRepository regTiradaRepository, + IRegPublicacionSeccionRepository regPublicacionSeccionRepository, + IPublicacionRepository publicacionRepository, + IPlantaRepository plantaRepository, + IPubliSeccionRepository publiSeccionRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _regTiradaRepository = regTiradaRepository; + _regPublicacionSeccionRepository = regPublicacionSeccionRepository; + _publicacionRepository = publicacionRepository; + _plantaRepository = plantaRepository; + _publiSeccionRepository = publiSeccionRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> ObtenerTiradasAsync(DateTime? fecha, int? idPublicacion, int? idPlanta) + { + var tiradasPrincipales = await _regTiradaRepository.GetByCriteriaAsync(fecha, idPublicacion, idPlanta); + var resultado = new List(); + + foreach (var tiradaP in tiradasPrincipales) + { + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(tiradaP.IdPublicacion); + var planta = await _plantaRepository.GetByIdAsync(tiradaP.IdPlanta); + var seccionesImpresas = await _regPublicacionSeccionRepository.GetByFechaPublicacionPlantaAsync(tiradaP.Fecha, tiradaP.IdPublicacion, tiradaP.IdPlanta); + + var detallesSeccionDto = new List(); + int totalPaginas = 0; + foreach (var seccionImp in seccionesImpresas) + { + var seccionInfo = await _publiSeccionRepository.GetByIdAsync(seccionImp.IdSeccion); + detallesSeccionDto.Add(new DetalleSeccionEnListadoDto + { + IdSeccion = seccionImp.IdSeccion, + NombreSeccion = seccionInfo?.Nombre ?? "Sección Desconocida", + CantPag = seccionImp.CantPag, + IdRegPublicacionSeccion = seccionImp.IdTirada // Este es el PK de bob_RegPublicaciones + }); + totalPaginas += seccionImp.CantPag; + } + + resultado.Add(new TiradaDto + { + IdRegistroTirada = tiradaP.IdRegistro, + IdPublicacion = tiradaP.IdPublicacion, + NombrePublicacion = publicacion?.Nombre ?? "Publicación Desconocida", + Fecha = tiradaP.Fecha.ToString("yyyy-MM-dd"), + IdPlanta = tiradaP.IdPlanta, + NombrePlanta = planta?.Nombre ?? "Planta Desconocida", + Ejemplares = tiradaP.Ejemplares, + SeccionesImpresas = detallesSeccionDto, + TotalPaginasSumadas = totalPaginas + }); + } + return resultado; + } + + + public async Task<(TiradaDto? TiradaCreada, string? Error)> RegistrarTiradaCompletaAsync(CreateTiradaRequestDto createDto, int idUsuario) + { + // Validaciones previas + var publicacion = await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion); + if (publicacion == null) return (null, "La publicación especificada no existe."); + var planta = await _plantaRepository.GetByIdAsync(createDto.IdPlanta); + if (planta == null) return (null, "La planta especificada no existe."); + + // Validar que no exista ya una tirada para esa Publicación, Fecha y Planta + // (bob_RegTiradas debería ser único por estos campos) + if (await _regTiradaRepository.GetByFechaPublicacionPlantaAsync(createDto.Fecha.Date, createDto.IdPublicacion, createDto.IdPlanta) != null) + { + return (null, $"Ya existe una tirada registrada para la publicación '{publicacion.Nombre}' en la planta '{planta.Nombre}' para la fecha {createDto.Fecha:dd/MM/yyyy}."); + } + + + // Validar secciones + foreach (var seccionDto in createDto.Secciones) + { + var seccionDb = await _publiSeccionRepository.GetByIdAsync(seccionDto.IdSeccion); + if (seccionDb == null || seccionDb.IdPublicacion != createDto.IdPublicacion) + return (null, $"La sección con ID {seccionDto.IdSeccion} no es válida o no pertenece a la publicación seleccionada."); + if (!seccionDb.Estado) // Asumiendo que solo se pueden tirar secciones activas + return (null, $"La sección '{seccionDb.Nombre}' no está activa y no puede incluirse en la tirada."); + } + + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + // 1. Crear registro en bob_RegTiradas (total de ejemplares) + var nuevaRegTirada = new RegTirada + { + IdPublicacion = createDto.IdPublicacion, + Fecha = createDto.Fecha.Date, + IdPlanta = createDto.IdPlanta, + Ejemplares = createDto.Ejemplares + }; + var regTiradaCreada = await _regTiradaRepository.CreateAsync(nuevaRegTirada, idUsuario, transaction); + if (regTiradaCreada == null) throw new DataException("Error al registrar el total de la tirada."); + + // 2. Crear registros en bob_RegPublicaciones (detalle de secciones y páginas) + var seccionesImpresasDto = new List(); + int totalPaginasSumadas = 0; + + foreach (var seccionDto in createDto.Secciones) + { + var nuevaRegPubSeccion = new RegPublicacionSeccion + { + IdPublicacion = createDto.IdPublicacion, + IdSeccion = seccionDto.IdSeccion, + CantPag = seccionDto.CantPag, + Fecha = createDto.Fecha.Date, + IdPlanta = createDto.IdPlanta + }; + var seccionCreadaEnTirada = await _regPublicacionSeccionRepository.CreateAsync(nuevaRegPubSeccion, idUsuario, transaction); + if (seccionCreadaEnTirada == null) throw new DataException($"Error al registrar la sección ID {seccionDto.IdSeccion} en la tirada."); + + var seccionInfo = await _publiSeccionRepository.GetByIdAsync(seccionDto.IdSeccion); // Para obtener nombre + seccionesImpresasDto.Add(new DetalleSeccionEnListadoDto{ + IdSeccion = seccionCreadaEnTirada.IdSeccion, + NombreSeccion = seccionInfo?.Nombre ?? "N/A", + CantPag = seccionCreadaEnTirada.CantPag, + IdRegPublicacionSeccion = seccionCreadaEnTirada.IdTirada + }); + totalPaginasSumadas += seccionCreadaEnTirada.CantPag; + } + + transaction.Commit(); + _logger.LogInformation("Tirada completa registrada para Pub ID {IdPub}, Fecha {Fecha}, Planta ID {IdPlanta} por Usuario ID {UserId}.", + createDto.IdPublicacion, createDto.Fecha.Date, createDto.IdPlanta, idUsuario); + + return (new TiradaDto { + IdRegistroTirada = regTiradaCreada.IdRegistro, + IdPublicacion = regTiradaCreada.IdPublicacion, + NombrePublicacion = publicacion.Nombre, + Fecha = regTiradaCreada.Fecha.ToString("yyyy-MM-dd"), + IdPlanta = regTiradaCreada.IdPlanta, + NombrePlanta = planta.Nombre, + Ejemplares = regTiradaCreada.Ejemplares, + SeccionesImpresas = seccionesImpresasDto, + TotalPaginasSumadas = totalPaginasSumadas + }, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error RegistrarTiradaCompletaAsync para Pub ID {IdPub}, Fecha {Fecha}", createDto.IdPublicacion, createDto.Fecha); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarTiradaCompletaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario) + { + // Verificar que la tirada principal exista + var tiradaPrincipal = await _regTiradaRepository.GetByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta); + if (tiradaPrincipal == null) + { + return (false, "No se encontró una tirada principal para los criterios especificados."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + // 1. Eliminar detalles de secciones de bob_RegPublicaciones + // El método ya guarda en historial + await _regPublicacionSeccionRepository.DeleteByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta, idUsuario, transaction); + _logger.LogInformation("Secciones de tirada eliminadas para Fecha: {Fecha}, PubID: {IdPublicacion}, PlantaID: {IdPlanta}", fecha.Date, idPublicacion, idPlanta); + + // 2. Eliminar el registro principal de bob_RegTiradas + // El método ya guarda en historial + bool principalEliminado = await _regTiradaRepository.DeleteByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta, idUsuario, transaction); + // bool principalEliminado = await _regTiradaRepository.DeleteAsync(tiradaPrincipal.IdRegistro, idUsuario, transaction); // Alternativa si ya tienes el IdRegistro + if (!principalEliminado && tiradaPrincipal != null) // Si DeleteByFechaPublicacionPlantaAsync devuelve false porque no encontró nada (raro si pasó la validación) + { + _logger.LogWarning("No se eliminó el registro principal de tirada (bob_RegTiradas) para Fecha: {Fecha}, PubID: {IdPublicacion}, PlantaID: {IdPlanta}. Pudo haber sido eliminado concurrentemente.", fecha.Date, idPublicacion, idPlanta); + // Decidir si esto es un error que debe hacer rollback + } + _logger.LogInformation("Registro principal de tirada eliminado para Fecha: {Fecha}, PubID: {IdPublicacion}, PlantaID: {IdPlanta}", fecha.Date, idPublicacion, idPlanta); + + + transaction.Commit(); + return (true, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error EliminarTiradaCompletaAsync para Fecha {Fecha}, PubID {IdPub}, PlantaID {IdPlanta}", fecha.Date, idPublicacion, idPlanta); + return (false, $"Error interno al eliminar la tirada: {ex.Message}"); + } + } + } +} \ 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 9b5f41b..f9fcd33 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+daf84d27081c399bf1dbd5db88606c4b562cee46")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+b6ba52f074807c7a2fddc76ab3cc2c45c446c1f8")] [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 6799a32..0e9f146 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":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","cInNtDBnkQU/n2nz4dl\u002BN2Fu06kTT6IORBeDdunxooU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","kULolnJcJq9du0a0dBwZaPVupTEFX15sai6mOONU2qk="],"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 9179fe7..dbe78cf 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":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","cInNtDBnkQU/n2nz4dl\u002BN2Fu06kTT6IORBeDdunxooU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","kULolnJcJq9du0a0dBwZaPVupTEFX15sai6mOONU2qk="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx b/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx new file mode 100644 index 0000000..89c0894 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx @@ -0,0 +1,227 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio +} from '@mui/material'; +import type { EntradaSalidaDistDto } from '../../../models/dtos/Distribucion/EntradaSalidaDistDto'; +import type { CreateEntradaSalidaDistDto } from '../../../models/dtos/Distribucion/CreateEntradaSalidaDistDto'; +import type { UpdateEntradaSalidaDistDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto'; +import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; +import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto'; +import publicacionService from '../../../services/Distribucion/publicacionService'; +import distribuidorService from '../../../services/Distribucion/distribuidorService'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 550 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface EntradaSalidaDistFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => Promise; + initialData?: EntradaSalidaDistDto | null; // Para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const EntradaSalidaDistFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idPublicacion, setIdPublicacion] = useState(''); + const [idDistribuidor, setIdDistribuidor] = useState(''); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [tipoMovimiento, setTipoMovimiento] = useState<'Salida' | 'Entrada'>('Salida'); + const [cantidad, setCantidad] = useState(''); + const [remito, setRemito] = useState(''); + const [observacion, setObservacion] = useState(''); + + const [publicaciones, setPublicaciones] = useState([]); + const [distribuidores, setDistribuidores] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + try { + const [pubsData, distData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas + distribuidorService.getAllDistribuidores() + ]); + setPublicaciones(pubsData); + setDistribuidores(distData); + } catch (error) { + console.error("Error al cargar datos para dropdowns", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open) { + fetchDropdownData(); + setIdPublicacion(initialData?.idPublicacion || ''); + setIdDistribuidor(initialData?.idDistribuidor || ''); + setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]); + setTipoMovimiento(initialData?.tipoMovimiento || 'Salida'); + setCantidad(initialData?.cantidad?.toString() || ''); + setRemito(initialData?.remito?.toString() || ''); + setObservacion(initialData?.observacion || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; + if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.'; + 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.'; + if (!tipoMovimiento) errors.tipoMovimiento = 'Seleccione un tipo de movimiento.'; + if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) { + errors.cantidad = 'La cantidad debe ser un número positivo.'; + } + if (!isEditing && (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0)) { + errors.remito = 'El Nro. Remito es obligatorio y debe ser un número positivo.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + 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: UpdateEntradaSalidaDistDto = { + cantidad: parseInt(cantidad, 10), + observacion: observacion || undefined, + }; + await onSubmit(dataToSubmit, initialData.idParte); + } else { + const dataToSubmit: CreateEntradaSalidaDistDto = { + idPublicacion: Number(idPublicacion), + idDistribuidor: Number(idDistribuidor), + fecha, + tipoMovimiento, + cantidad: parseInt(cantidad, 10), + remito: parseInt(remito, 10), + observacion: observacion || undefined, + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de EntradaSalidaDistFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Movimiento Distribuidor' : 'Registrar Movimiento Distribuidor'} + + + + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + + + Distribuidor + + {localErrors.idDistribuidor && {localErrors.idDistribuidor}} + + + {setFecha(e.target.value); handleInputChange('fecha');}} + margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + + + Tipo de Movimiento + {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} > + } label="Salida (a Distribuidor)" disabled={loading || isEditing}/> + } label="Entrada (de Distribuidor)" disabled={loading || isEditing}/> + + {localErrors.tipoMovimiento && {localErrors.tipoMovimiento}} + + + {setRemito(e.target.value); handleInputChange('remito');}} + margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''} + disabled={loading || isEditing} inputProps={{min:1}} + /> + + {setCantidad(e.target.value); handleInputChange('cantidad');}} + margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''} + disabled={loading} inputProps={{min:1}} + /> + setObservacion(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default EntradaSalidaDistFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/PorcMonCanillaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/PorcMonCanillaFormModal.tsx new file mode 100644 index 0000000..7be0bf2 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/PorcMonCanillaFormModal.tsx @@ -0,0 +1,209 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox, InputAdornment +} from '@mui/material'; +import type { PorcMonCanillaDto } from '../../../models/dtos/Distribucion/PorcMonCanillaDto'; +import type { CreatePorcMonCanillaDto } from '../../../models/dtos/Distribucion/CreatePorcMonCanillaDto'; +import type { UpdatePorcMonCanillaDto } from '../../../models/dtos/Distribucion/UpdatePorcMonCanillaDto'; +import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Para el dropdown +import canillaService from '../../../services/Distribucion/canillaService'; // Para cargar canillitas + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 550 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface PorcMonCanillaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => Promise; + idPublicacion: number; + initialData?: PorcMonCanillaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PorcMonCanillaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + idPublicacion, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idCanilla, setIdCanilla] = useState(''); + const [vigenciaD, setVigenciaD] = useState(''); + const [vigenciaH, setVigenciaH] = useState(''); + const [porcMon, setPorcMon] = useState(''); + const [esPorcentaje, setEsPorcentaje] = useState(true); // Default a porcentaje + + const [canillitas, setCanillitas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingCanillitas, setLoadingCanillitas] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchCanillitas = async () => { + setLoadingCanillitas(true); + try { + // Aquí podríamos querer filtrar solo canillitas accionistas si la regla de negocio lo impone + // o todos los activos. Por ahora, todos los activos. + const data = await canillaService.getAllCanillas(undefined, undefined, true); + setCanillitas(data); + } catch (error) { + console.error("Error al cargar canillitas", error); + setLocalErrors(prev => ({...prev, canillitas: 'Error al cargar canillitas.'})); + } finally { + setLoadingCanillitas(false); + } + }; + + if (open) { + fetchCanillitas(); + setIdCanilla(initialData?.idCanilla || ''); + setVigenciaD(initialData?.vigenciaD || ''); + setVigenciaH(initialData?.vigenciaH || ''); + setPorcMon(initialData?.porcMon?.toString() || ''); + setEsPorcentaje(initialData ? initialData.esPorcentaje : true); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idCanilla) errors.idCanilla = 'Debe seleccionar un canillita.'; + if (!isEditing && !vigenciaD.trim()) { + errors.vigenciaD = 'La Vigencia Desde es obligatoria.'; + } else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) { + errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).'; + } + if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) { + errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).'; + } else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) { + errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.'; + } + if (!porcMon.trim()) errors.porcMon = 'El valor es obligatorio.'; + else { + const numVal = parseFloat(porcMon); + if (isNaN(numVal) || numVal < 0) { + errors.porcMon = 'El valor debe ser un número positivo.'; + } else if (esPorcentaje && numVal > 100) { + errors.porcMon = 'El porcentaje no puede ser mayor a 100.'; + } + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + 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 { + const valorNum = parseFloat(porcMon); + + if (isEditing && initialData) { + const dataToSubmit: UpdatePorcMonCanillaDto = { + porcMon: valorNum, + esPorcentaje, + vigenciaH: vigenciaH.trim() ? vigenciaH : null, + }; + await onSubmit(dataToSubmit, initialData.idPorcMon); + } else { + const dataToSubmit: CreatePorcMonCanillaDto = { + idPublicacion, + idCanilla: Number(idCanilla), + vigenciaD, + porcMon: valorNum, + esPorcentaje, + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PorcMonCanillaFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Porcentaje/Monto Canillita' : 'Agregar Nuevo Porcentaje/Monto Canillita'} + + + + Canillita + + {localErrors.idCanilla && {localErrors.idCanilla}} + + {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}} + margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + {isEditing && ( + {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}} + margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''} + disabled={loading} InputLabelProps={{ shrink: true }} + /> + )} + {setPorcMon(e.target.value); handleInputChange('porcMon');}} + margin="dense" fullWidth error={!!localErrors.porcMon} helperText={localErrors.porcMon || ''} + disabled={loading} + InputProps={{ startAdornment: esPorcentaje ? undefined : $, + endAdornment: esPorcentaje ? % : undefined }} + inputProps={{ step: "0.01", lang:"es-AR" }} + /> + setEsPorcentaje(e.target.checked)} disabled={loading}/>} + label="Es Porcentaje (si no, es Monto Fijo)" sx={{mt:1}} + /> + + {errorMessage && {errorMessage}} + {localErrors.canillitas && {localErrors.canillitas}} + + + + + + + + + ); +}; + +export default PorcMonCanillaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/PorcPagoFormModal.tsx b/Frontend/src/components/Modals/Distribucion/PorcPagoFormModal.tsx new file mode 100644 index 0000000..09edfc8 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/PorcPagoFormModal.tsx @@ -0,0 +1,196 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, InputAdornment +} from '@mui/material'; +import type { PorcPagoDto } from '../../../models/dtos/Distribucion/PorcPagoDto'; +import type { CreatePorcPagoDto } from '../../../models/dtos/Distribucion/CreatePorcPagoDto'; +import type { UpdatePorcPagoDto } from '../../../models/dtos/Distribucion/UpdatePorcPagoDto'; +import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto'; +import distribuidorService from '../../../services/Distribucion/distribuidorService'; // Para cargar distribuidores + +const modalStyle = { /* ... (mismo estilo) ... */ + 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 PorcPagoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePorcPagoDto | UpdatePorcPagoDto, idPorcentaje?: number) => Promise; + idPublicacion: number; + initialData?: PorcPagoDto | null; // Para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PorcPagoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + idPublicacion, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idDistribuidor, setIdDistribuidor] = useState(''); + const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd" + const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd" + const [porcentaje, setPorcentaje] = useState(''); + + const [distribuidores, setDistribuidores] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDistribuidores, setLoadingDistribuidores] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchDistribuidores = async () => { + setLoadingDistribuidores(true); + try { + const data = await distribuidorService.getAllDistribuidores(); + setDistribuidores(data); + } catch (error) { + console.error("Error al cargar distribuidores", error); + setLocalErrors(prev => ({...prev, distribuidores: 'Error al cargar distribuidores.'})); + } finally { + setLoadingDistribuidores(false); + } + }; + + if (open) { + fetchDistribuidores(); + setIdDistribuidor(initialData?.idDistribuidor || ''); + setVigenciaD(initialData?.vigenciaD || ''); + setVigenciaH(initialData?.vigenciaH || ''); + setPorcentaje(initialData?.porcentaje?.toString() || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.'; + if (!isEditing && !vigenciaD.trim()) { + errors.vigenciaD = 'La Vigencia Desde es obligatoria.'; + } else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) { + errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).'; + } + if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) { + errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).'; + } else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) { + errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.'; + } + if (!porcentaje.trim()) errors.porcentaje = 'El porcentaje es obligatorio.'; + else { + const porcNum = parseFloat(porcentaje); + if (isNaN(porcNum) || porcNum < 0 || porcNum > 100) { + errors.porcentaje = 'El porcentaje debe ser un número entre 0 y 100.'; + } + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + 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 { + const porcentajeNum = parseFloat(porcentaje); + + if (isEditing && initialData) { + const dataToSubmit: UpdatePorcPagoDto = { + porcentaje: porcentajeNum, + vigenciaH: vigenciaH.trim() ? vigenciaH : null, + }; + await onSubmit(dataToSubmit, initialData.idPorcentaje); + } else { + const dataToSubmit: CreatePorcPagoDto = { + idPublicacion, + idDistribuidor: Number(idDistribuidor), + vigenciaD, + porcentaje: porcentajeNum, + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PorcPagoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Porcentaje de Pago' : 'Agregar Nuevo Porcentaje de Pago'} + + + + Distribuidor + + {localErrors.idDistribuidor && {localErrors.idDistribuidor}} + + {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}} + margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + {isEditing && ( + {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}} + margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''} + disabled={loading} InputLabelProps={{ shrink: true }} + /> + )} + {setPorcentaje(e.target.value); handleInputChange('porcentaje');}} + margin="dense" fullWidth error={!!localErrors.porcentaje} helperText={localErrors.porcentaje || ''} + disabled={loading} + InputProps={{ endAdornment: % }} + inputProps={{ step: "0.01", lang:"es-AR" }} + /> + + {errorMessage && {errorMessage}} + {localErrors.distribuidores && {localErrors.distribuidores}} + + + + + + + + + ); +}; + +export default PorcPagoFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/PubliSeccionFormModal.tsx b/Frontend/src/components/Modals/Distribucion/PubliSeccionFormModal.tsx new file mode 100644 index 0000000..377d01b --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/PubliSeccionFormModal.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControlLabel, Checkbox +} from '@mui/material'; +import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; +import type { CreatePubliSeccionDto } from '../../../models/dtos/Distribucion/CreatePubliSeccionDto'; +import type { UpdatePubliSeccionDto } from '../../../models/dtos/Distribucion/UpdatePubliSeccionDto'; + +const modalStyle = { /* ... (mismo estilo) ... */ + 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: 4, +}; + +interface PubliSeccionFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePubliSeccionDto | UpdatePubliSeccionDto, idSeccion?: number) => Promise; + idPublicacion: number; // Siempre necesario para la creación + initialData?: PubliSeccionDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PubliSeccionFormModal: React.FC = ({ + open, + onClose, + onSubmit, + idPublicacion, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [estado, setEstado] = useState(true); // Default a activa + + const [loading, setLoading] = useState(false); + const [localErrorNombre, setLocalErrorNombre] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombre(initialData?.nombre || ''); + setEstado(initialData ? initialData.estado : true); + setLocalErrorNombre(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + let isValid = true; + if (!nombre.trim()) { + setLocalErrorNombre('El nombre de la sección es obligatorio.'); + isValid = false; + } + return isValid; + }; + + const handleInputChange = () => { + if (localErrorNombre) setLocalErrorNombre(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + if (isEditing && initialData) { + const dataToSubmit: UpdatePubliSeccionDto = { nombre, estado }; + await onSubmit(dataToSubmit, initialData.idSeccion); + } else { + const dataToSubmit: CreatePubliSeccionDto = { + idPublicacion, // Viene de props + nombre, + estado + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PubliSeccionFormModal:", error); + // El error de API se maneja en la página + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Sección' : 'Agregar Nueva Sección'} + + + {setNombre(e.target.value); handleInputChange();}} + margin="dense" fullWidth error={!!localErrorNombre} helperText={localErrorNombre || ''} + disabled={loading} autoFocus + /> + setEstado(e.target.checked)} disabled={loading}/>} + label="Activa" sx={{mt:1}} + /> + + {errorMessage && {errorMessage}} + + + + + + + + + ); +}; + +export default PubliSeccionFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/RecargoZonaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/RecargoZonaFormModal.tsx new file mode 100644 index 0000000..96b0eed --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/RecargoZonaFormModal.tsx @@ -0,0 +1,193 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, InputAdornment +} from '@mui/material'; +import type { RecargoZonaDto } from '../../../models/dtos/Distribucion/RecargoZonaDto'; +import type { CreateRecargoZonaDto } from '../../../models/dtos/Distribucion/CreateRecargoZonaDto'; +import type { UpdateRecargoZonaDto } from '../../../models/dtos/Distribucion/UpdateRecargoZonaDto'; +import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas +import zonaService from '../../../services/Distribucion/zonaService'; // Para cargar zonas + +const modalStyle = { /* ... (mismo estilo) ... */ + 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 RecargoZonaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => Promise; + idPublicacion: number; + initialData?: RecargoZonaDto | null; // Para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const RecargoZonaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + idPublicacion, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idZona, setIdZona] = useState(''); + const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd" + const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd" + const [valor, setValor] = useState(''); + + const [zonas, setZonas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingZonas, setLoadingZonas] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchZonas = async () => { + setLoadingZonas(true); + try { + const data = await zonaService.getAllZonas(); // Asume que devuelve zonas activas + setZonas(data); + } catch (error) { + console.error("Error al cargar zonas", error); + setLocalErrors(prev => ({...prev, zonas: 'Error al cargar zonas.'})); + } finally { + setLoadingZonas(false); + } + }; + + if (open) { + fetchZonas(); + setIdZona(initialData?.idZona || ''); + setVigenciaD(initialData?.vigenciaD || ''); + setVigenciaH(initialData?.vigenciaH || ''); + setValor(initialData?.valor?.toString() || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idZona) errors.idZona = 'Debe seleccionar una zona.'; + if (!isEditing && !vigenciaD.trim()) { + errors.vigenciaD = 'La Vigencia Desde es obligatoria.'; + } else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) { + errors.vigenciaD = 'Formato de Vigencia Desde inválido (YYYY-MM-DD).'; + } + if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) { + errors.vigenciaH = 'Formato de Vigencia Hasta inválido (YYYY-MM-DD).'; + } else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) { + errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.'; + } + if (!valor.trim()) errors.valor = 'El valor es obligatorio.'; + else if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) { + errors.valor = 'El valor debe ser un número positivo.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + 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 { + const valorNum = parseFloat(valor); + + if (isEditing && initialData) { + const dataToSubmit: UpdateRecargoZonaDto = { + valor: valorNum, + vigenciaH: vigenciaH.trim() ? vigenciaH : null, + }; + await onSubmit(dataToSubmit, initialData.idRecargo); + } else { + const dataToSubmit: CreateRecargoZonaDto = { + idPublicacion, + idZona: Number(idZona), + vigenciaD, + valor: valorNum, + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de RecargoZonaFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Recargo por Zona' : 'Agregar Nuevo Recargo por Zona'} + + + + Zona + + {localErrors.idZona && {localErrors.idZona}} + + {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}} + margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + {isEditing && ( + {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}} + margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''} + disabled={loading} InputLabelProps={{ shrink: true }} + /> + )} + {setValor(e.target.value); handleInputChange('valor');}} + margin="dense" fullWidth error={!!localErrors.valor} helperText={localErrors.valor || ''} + disabled={loading} + InputProps={{ startAdornment: $ }} + inputProps={{ step: "0.01", lang:"es-AR" }} + /> + + {errorMessage && {errorMessage}} + {localErrors.zonas && {localErrors.zonas}} + + + + + + + + + ); +}; + +export default RecargoZonaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/SalidaOtroDestinoFormModal.tsx b/Frontend/src/components/Modals/Distribucion/SalidaOtroDestinoFormModal.tsx new file mode 100644 index 0000000..b4bb2a4 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/SalidaOtroDestinoFormModal.tsx @@ -0,0 +1,199 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem +} from '@mui/material'; +import type { SalidaOtroDestinoDto } from '../../../models/dtos/Distribucion/SalidaOtroDestinoDto'; +import type { CreateSalidaOtroDestinoDto } from '../../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto'; +import type { UpdateSalidaOtroDestinoDto } from '../../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto'; +import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; +import type { OtroDestinoDto } from '../../../models/dtos/Distribucion/OtroDestinoDto'; +import publicacionService from '../../../services/Distribucion/publicacionService'; +import otroDestinoService from '../../../services/Distribucion/otroDestinoService'; + +const modalStyle = { /* ... (mismo estilo) ... */ + 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 SalidaOtroDestinoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateSalidaOtroDestinoDto | UpdateSalidaOtroDestinoDto, idParte?: number) => Promise; + initialData?: SalidaOtroDestinoDto | null; // Para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const SalidaOtroDestinoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idPublicacion, setIdPublicacion] = useState(''); + const [idDestino, setIdDestino] = useState(''); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [cantidad, setCantidad] = useState(''); + const [observacion, setObservacion] = useState(''); + + const [publicaciones, setPublicaciones] = useState([]); + const [otrosDestinos, setOtrosDestinos] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + try { + const [pubsData, destinosData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas + otroDestinoService.getAllOtrosDestinos() + ]); + setPublicaciones(pubsData); + setOtrosDestinos(destinosData); + } catch (error) { + console.error("Error al cargar datos para dropdowns", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open) { + fetchDropdownData(); + setIdPublicacion(initialData?.idPublicacion || ''); + setIdDestino(initialData?.idDestino || ''); + setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]); + setCantidad(initialData?.cantidad?.toString() || ''); + setObservacion(initialData?.observacion || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; + if (!idDestino) errors.idDestino = 'Seleccione un destino.'; + 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.'; + if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) { + errors.cantidad = 'La cantidad debe ser un número positivo.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + 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: UpdateSalidaOtroDestinoDto = { + cantidad: parseInt(cantidad, 10), + observacion: observacion || undefined, + }; + await onSubmit(dataToSubmit, initialData.idParte); + } else { + const dataToSubmit: CreateSalidaOtroDestinoDto = { + idPublicacion: Number(idPublicacion), + idDestino: Number(idDestino), + fecha, + cantidad: parseInt(cantidad, 10), + observacion: observacion || undefined, + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de SalidaOtroDestinoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Salida a Otro Destino' : 'Registrar Salida a Otro Destino'} + + + + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + + Otro Destino + + {localErrors.idDestino && {localErrors.idDestino}} + + {setFecha(e.target.value); handleInputChange('fecha');}} + margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + {setCantidad(e.target.value); handleInputChange('cantidad');}} + margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''} + disabled={loading} inputProps={{min:1}} + /> + setObservacion(e.target.value)} + margin="dense" fullWidth multiline rows={3} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default SalidaOtroDestinoFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx b/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx new file mode 100644 index 0000000..dd90fc5 --- /dev/null +++ b/Frontend/src/components/Modals/Impresion/StockBobinaCambioEstadoModal.tsx @@ -0,0 +1,261 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem +} from '@mui/material'; +import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto'; +import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto'; +import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto'; +import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; +import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; // Asumiendo que tienes este DTO +import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; // Para cargar estados +import publicacionService from '../../../services/Distribucion/publicacionService'; // Para cargar publicaciones +import publiSeccionService from '../../../services/Distribucion/publiSeccionService'; // Para cargar secciones + +const modalStyle = { /* ... (mismo estilo) ... */ + 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' +}; + +// IDs de estados conocidos (ajusta según tu BD) +const ID_ESTADO_EN_USO = 2; +const ID_ESTADO_DANADA = 3; +// const ID_ESTADO_DISPONIBLE = 1; // No se cambia a Disponible desde este modal + +interface StockBobinaCambioEstadoModalProps { + open: boolean; + onClose: () => void; + onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise; + bobinaActual: StockBobinaDto | null; // La bobina cuyo estado se va a cambiar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const StockBobinaCambioEstadoModal: React.FC = ({ + open, + onClose, + onSubmit, + bobinaActual, + errorMessage, + clearErrorMessage +}) => { + const [nuevoEstadoId, setNuevoEstadoId] = useState(''); + const [idPublicacion, setIdPublicacion] = useState(''); + const [idSeccion, setIdSeccion] = useState(''); + const [obs, setObs] = useState(''); + const [fechaCambioEstado, setFechaCambioEstado] = useState(''); + + const [estadosDisponibles, setEstadosDisponibles] = useState([]); + const [publicacionesDisponibles, setPublicacionesDisponibles] = useState([]); + const [seccionesDisponibles, setSeccionesDisponibles] = useState([]); + + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + useEffect(() => { + const fetchDropdownData = async () => { + if (!bobinaActual) return; + setLoadingDropdowns(true); + try { + const estadosData = await estadoBobinaService.getAllEstadosBobina(); + // Filtrar estados: no se puede volver a "Disponible" o al mismo estado actual desde aquí. + // Y si está "Dañada", no se puede cambiar. + let estadosFiltrados = estadosData.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina && e.idEstadoBobina !== 1); + if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) { // Si ya está dañada, no hay más cambios + estadosFiltrados = []; + } else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) { // Si está en uso, solo puede pasar a Dañada + estadosFiltrados = estadosData.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA); + } + + + setEstadosDisponibles(estadosFiltrados); + + if (estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO)) { // Solo cargar publicaciones si "En Uso" es una opción + const publicacionesData = await publicacionService.getAllPublicaciones(undefined, undefined, true); // Solo habilitadas + setPublicacionesDisponibles(publicacionesData); + } else { + setPublicacionesDisponibles([]); + } + + } catch (error) { + console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open && bobinaActual) { + fetchDropdownData(); + setNuevoEstadoId(''); + setIdPublicacion(''); + setIdSeccion(''); + setObs(bobinaActual.obs || ''); // Pre-cargar obs existente + setFechaCambioEstado(new Date().toISOString().split('T')[0]); // Default a hoy + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, bobinaActual, clearErrorMessage]); + + + // Cargar secciones cuando cambia la publicación seleccionada y el estado es "En Uso" + useEffect(() => { + const fetchSecciones = async () => { + if (nuevoEstadoId === ID_ESTADO_EN_USO && idPublicacion) { + setLoadingDropdowns(true); // Podrías tener un loader específico para secciones + try { + const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); // Solo activas + setSeccionesDisponibles(data); + } catch (error) { + console.error("Error al cargar secciones:", error); + setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'})); + } finally { + setLoadingDropdowns(false); + } + } else { + setSeccionesDisponibles([]); // Limpiar secciones si no aplica + } + }; + fetchSecciones(); + }, [nuevoEstadoId, idPublicacion]); + + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.'; + if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha de cambio es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.'; + + if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { + if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; + if (!idSeccion) errors.idSeccion = 'Seleccione una sección.'; + } + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + if (fieldName === 'nuevoEstadoId') { // Si cambia el estado, resetear pub/secc + setIdPublicacion(''); + setIdSeccion(''); + } + if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion + setIdSeccion(''); + } + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate() || !bobinaActual) return; + + setLoading(true); + try { + const dataToSubmit: CambiarEstadoBobinaDto = { + nuevoEstadoId: Number(nuevoEstadoId), + idPublicacion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idPublicacion) : null, + idSeccion: Number(nuevoEstadoId) === ID_ESTADO_EN_USO ? Number(idSeccion) : null, + obs: obs || undefined, + fechaCambioEstado, + }; + await onSubmit(bobinaActual.idBobina, dataToSubmit); + onClose(); + } catch (error: any) { + console.error("Error en submit de StockBobinaCambioEstadoModal:", error); + } finally { + setLoading(false); + } + }; + + if (!bobinaActual) return null; + + return ( + + + + Cambiar Estado de Bobina: {bobinaActual.nroBobina} + + + Estado Actual: {bobinaActual.nombreEstadoBobina} + + + + + Nuevo Estado + + {localErrors.nuevoEstadoId && {localErrors.nuevoEstadoId}} + + + {Number(nuevoEstadoId) === ID_ESTADO_EN_USO && ( + <> + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + + Sección + + {localErrors.idSeccion && {localErrors.idSeccion}} + {localErrors.secciones && {localErrors.secciones}} + + + )} + + {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}} + margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''} + disabled={loading} InputLabelProps={{ shrink: true }} + /> + setObs(e.target.value)} + margin="dense" fullWidth multiline rows={3} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default StockBobinaCambioEstadoModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Impresion/StockBobinaEditFormModal.tsx b/Frontend/src/components/Modals/Impresion/StockBobinaEditFormModal.tsx new file mode 100644 index 0000000..3fd3363 --- /dev/null +++ b/Frontend/src/components/Modals/Impresion/StockBobinaEditFormModal.tsx @@ -0,0 +1,206 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem +} from '@mui/material'; +import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto'; +import type { UpdateStockBobinaDto } from '../../../models/dtos/Impresion/UpdateStockBobinaDto'; +import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto'; +import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto'; +import tipoBobinaService from '../../../services/Impresion/tipoBobinaService'; +import plantaService from '../../../services/Impresion/plantaService'; + +const modalStyle = { /* ... (mismo estilo que StockBobinaIngresoFormModal) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 550 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface StockBobinaEditFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (idBobina: number, data: UpdateStockBobinaDto) => Promise; + initialData: StockBobinaDto | null; // Siempre habrá initialData para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const StockBobinaEditFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idTipoBobina, setIdTipoBobina] = useState(''); + const [nroBobina, setNroBobina] = useState(''); + const [peso, setPeso] = useState(''); + const [idPlanta, setIdPlanta] = useState(''); + const [remito, setRemito] = useState(''); + const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd + + const [tiposBobina, setTiposBobina] = useState([]); + const [plantas, setPlantas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + try { + const [tiposData, plantasData] = await Promise.all([ + tipoBobinaService.getAllTiposBobina(), + plantaService.getAllPlantas() + ]); + setTiposBobina(tiposData); + setPlantas(plantasData); + } catch (error) { + console.error("Error al cargar datos para dropdowns (StockBobina Edit)", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open && initialData) { + fetchDropdownData(); + setIdTipoBobina(initialData.idTipoBobina || ''); + setNroBobina(initialData.nroBobina || ''); + setPeso(initialData.peso?.toString() || ''); + setIdPlanta(initialData.idPlanta || ''); + setRemito(initialData.remito || ''); + setFechaRemito(initialData.fechaRemito || ''); // Asume yyyy-MM-dd del DTO + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idTipoBobina) errors.idTipoBobina = 'Seleccione un tipo.'; + if (!nroBobina.trim()) errors.nroBobina = 'Nro. Bobina es obligatorio.'; + if (!peso.trim() || isNaN(parseInt(peso)) || parseInt(peso) <= 0) errors.peso = 'Peso debe ser un número positivo.'; + if (!idPlanta) errors.idPlanta = 'Seleccione una planta.'; + if (!remito.trim()) errors.remito = 'Remito es obligatorio.'; + if (!fechaRemito.trim()) errors.fechaRemito = 'Fecha de Remito es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaRemito)) errors.fechaRemito = 'Formato de fecha inválido.'; + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate() || !initialData) return; // initialData siempre debería existir aquí + + setLoading(true); + try { + const dataToSubmit: UpdateStockBobinaDto = { + idTipoBobina: Number(idTipoBobina), + nroBobina, + peso: parseInt(peso, 10), + idPlanta: Number(idPlanta), + remito, + fechaRemito, + }; + await onSubmit(initialData.idBobina, dataToSubmit); + onClose(); + } catch (error: any) { + console.error("Error en submit de StockBobinaEditFormModal:", error); + // El error de API lo maneja la página que llama a este modal + } finally { + setLoading(false); + } + }; + + if (!initialData) return null; // No renderizar si no hay datos iniciales (aunque open lo controla) + + return ( + + + + Editar Datos de Bobina (ID: {initialData.idBobina}) + + + + + + Tipo Bobina + + {localErrors.idTipoBobina && {localErrors.idTipoBobina}} + + {setNroBobina(e.target.value); handleInputChange('nroBobina');}} + margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''} + disabled={loading} sx={{flex:1, minWidth: '200px'}} autoFocus + /> + + + {setPeso(e.target.value); handleInputChange('peso');}} + margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''} + disabled={loading} sx={{flex:1, minWidth: '150px'}} + /> + + Planta Destino + + {localErrors.idPlanta && {localErrors.idPlanta}} + + + + {setRemito(e.target.value); handleInputChange('remito');}} + margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''} + disabled={loading} sx={{flex:1, minWidth: '200px'}} + /> + {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}} + margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''} + disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}} + /> + + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default StockBobinaEditFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Impresion/StockBobinaIngresoFormModal.tsx b/Frontend/src/components/Modals/Impresion/StockBobinaIngresoFormModal.tsx new file mode 100644 index 0000000..0bfdb1f --- /dev/null +++ b/Frontend/src/components/Modals/Impresion/StockBobinaIngresoFormModal.tsx @@ -0,0 +1,192 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; +import type { CreateStockBobinaDto } from '../../../models/dtos/Impresion/CreateStockBobinaDto'; +import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto'; +import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto'; +import tipoBobinaService from '../../../services/Impresion/tipoBobinaService'; +import plantaService from '../../../services/Impresion/plantaService'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 550 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface StockBobinaIngresoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateStockBobinaDto) => Promise; // Solo para crear + errorMessage?: string | null; + clearErrorMessage: () => void; + // initialData no es necesario para un modal de solo creación +} + +const StockBobinaIngresoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + errorMessage, + clearErrorMessage +}) => { + const [idTipoBobina, setIdTipoBobina] = useState(''); + const [nroBobina, setNroBobina] = useState(''); + const [peso, setPeso] = useState(''); + const [idPlanta, setIdPlanta] = useState(''); + const [remito, setRemito] = useState(''); + const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd + + const [tiposBobina, setTiposBobina] = useState([]); + const [plantas, setPlantas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + try { + const [tiposData, plantasData] = await Promise.all([ + tipoBobinaService.getAllTiposBobina(), + plantaService.getAllPlantas() + ]); + setTiposBobina(tiposData); + setPlantas(plantasData); + } catch (error) { + console.error("Error al cargar datos para dropdowns (StockBobina)", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar tipos/plantas.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open) { + fetchDropdownData(); + // Resetear campos + setIdTipoBobina(''); setNroBobina(''); setPeso(''); setIdPlanta(''); + setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]); // Default a hoy + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idTipoBobina) errors.idTipoBobina = 'Seleccione un tipo.'; + if (!nroBobina.trim()) errors.nroBobina = 'Nro. Bobina es obligatorio.'; + if (!peso.trim() || isNaN(parseInt(peso)) || parseInt(peso) <= 0) errors.peso = 'Peso debe ser un número positivo.'; + if (!idPlanta) errors.idPlanta = 'Seleccione una planta.'; + if (!remito.trim()) errors.remito = 'Remito es obligatorio.'; + if (!fechaRemito.trim()) errors.fechaRemito = 'Fecha de Remito es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaRemito)) errors.fechaRemito = 'Formato de fecha inválido.'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + 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 { + const dataToSubmit: CreateStockBobinaDto = { + idTipoBobina: Number(idTipoBobina), + nroBobina, + peso: parseInt(peso, 10), + idPlanta: Number(idPlanta), + remito, + fechaRemito, + }; + await onSubmit(dataToSubmit); + onClose(); + } catch (error: any) { + console.error("Error en submit de StockBobinaIngresoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + Ingresar Nueva Bobina a Stock + + + + Tipo Bobina + + {localErrors.idTipoBobina && {localErrors.idTipoBobina}} + + {setNroBobina(e.target.value); handleInputChange('nroBobina');}} + margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''} + disabled={loading} sx={{flex:1, minWidth: '200px'}} + /> + + + {setPeso(e.target.value); handleInputChange('peso');}} + margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''} + disabled={loading} sx={{flex:1, minWidth: '150px'}} + /> + + Planta Destino + + {localErrors.idPlanta && {localErrors.idPlanta}} + + + + {setRemito(e.target.value); handleInputChange('remito');}} + margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''} + disabled={loading} sx={{flex:1, minWidth: '200px'}} + /> + {setFechaRemito(e.target.value); handleInputChange('fechaRemito');}} + margin="dense" fullWidth error={!!localErrors.fechaRemito} helperText={localErrors.fechaRemito || ''} + disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: '200px'}} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default StockBobinaIngresoFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Impresion/TiradaFormModal.tsx b/Frontend/src/components/Modals/Impresion/TiradaFormModal.tsx new file mode 100644 index 0000000..56d20e1 --- /dev/null +++ b/Frontend/src/components/Modals/Impresion/TiradaFormModal.tsx @@ -0,0 +1,334 @@ +// src/components/Modals/TiradaFormModal.tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, IconButton, Paper, + Table, TableHead, TableRow, TableCell, TableBody, + TableContainer +} from '@mui/material'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import type { CreateTiradaRequestDto } from '../../../models/dtos/Impresion/CreateTiradaRequestDto'; +import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; +import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto'; +import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; +import publicacionService from '../../../services/Distribucion/publicacionService'; +import plantaService from '../../../services/Impresion/plantaService'; +import publiSeccionService from '../../../services/Distribucion/publiSeccionService'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '750px' }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 3, + maxHeight: '90vh', + overflowY: 'auto' +}; + +// CORREGIDO: Ajustar el tipo para los inputs. Usaremos string para los inputs, +// y convertiremos a number al hacer submit o al validar donde sea necesario. +interface DetalleSeccionFormState { + idSeccion: number | ''; // Permitir string vacío para el Select no seleccionado + nombreSeccion?: string; + cantPag: string; // TextField de cantPag siempre es string + idTemporal: string; // Para la key de React +} + + +interface TiradaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateTiradaRequestDto) => Promise; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const TiradaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + errorMessage, + clearErrorMessage +}) => { + const [idPublicacion, setIdPublicacion] = useState(''); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [idPlanta, setIdPlanta] = useState(''); + const [ejemplares, setEjemplares] = useState(''); + // CORREGIDO: Usar el nuevo tipo para el estado del formulario de secciones + const [seccionesDeTirada, setSeccionesDeTirada] = useState([]); + + const [publicaciones, setPublicaciones] = useState([]); + const [plantas, setPlantas] = useState([]); + const [seccionesPublicacion, setSeccionesPublicacion] = useState([]); + + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const resetForm = () => { + setIdPublicacion(''); + setFecha(new Date().toISOString().split('T')[0]); + setIdPlanta(''); + setEjemplares(''); + setSeccionesDeTirada([]); + setSeccionesPublicacion([]); + setLocalErrors({}); + clearErrorMessage(); + }; + + const fetchInitialDropdowns = useCallback(async () => { + setLoadingDropdowns(true); + try { + const [pubsData, plantasData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + plantaService.getAllPlantas() + ]); + setPublicaciones(pubsData); + setPlantas(plantasData); + } catch (error) { + console.error("Error al cargar publicaciones/plantas", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos iniciales.'})); + } finally { + setLoadingDropdowns(false); + } + }, []); + + useEffect(() => { + if (open) { + resetForm(); // Llama a resetForm aquí + fetchInitialDropdowns(); + } + }, [open, fetchInitialDropdowns]); // resetForm no necesita estar en las dependencias si su contenido no cambia basado en props/estado que también estén en las dependencias. + + const fetchSeccionesDePublicacion = useCallback(async (pubId: number) => { + if (!pubId) { + setSeccionesPublicacion([]); + setSeccionesDeTirada([]); + return; + } + setLoadingDropdowns(true); + try { + const data = await publiSeccionService.getSeccionesPorPublicacion(pubId, true); + setSeccionesPublicacion(data); + setSeccionesDeTirada([]); + } catch (error) { + console.error("Error al cargar secciones de la publicación", error); + setLocalErrors(prev => ({...prev, secciones: 'Error al cargar secciones.'})); + } finally { + setLoadingDropdowns(false); + } + }, []); + + useEffect(() => { + if (idPublicacion) { + fetchSeccionesDePublicacion(Number(idPublicacion)); + } else { + setSeccionesPublicacion([]); + setSeccionesDeTirada([]); + } + }, [idPublicacion, fetchSeccionesDePublicacion]); + + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; + 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.'; + if (!idPlanta) errors.idPlanta = 'Seleccione una planta.'; + if (!ejemplares.trim() || isNaN(parseInt(ejemplares)) || parseInt(ejemplares) <= 0) errors.ejemplares = 'Ejemplares debe ser un número positivo.'; + + if (seccionesDeTirada.length === 0) { + errors.seccionesArray = 'Debe agregar al menos una sección a la tirada.'; + } else { + seccionesDeTirada.forEach((sec, index) => { + if (sec.idSeccion === '') errors[`seccion_${index}_id`] = `Fila ${index + 1}: Debe seleccionar una sección.`; + if (!sec.cantPag.trim() || isNaN(Number(sec.cantPag)) || Number(sec.cantPag) <= 0) { + errors[`seccion_${index}_pag`] = `Fila ${index + 1}: Cant. Páginas debe ser un número positivo.`; + } + }); + } + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleAddSeccion = () => { + setSeccionesDeTirada([...seccionesDeTirada, { idSeccion: '', cantPag: '', nombreSeccion: '', idTemporal: crypto.randomUUID() }]); + if (localErrors.seccionesArray) setLocalErrors(prev => ({ ...prev, seccionesArray: null })); + }; + const handleRemoveSeccion = (index: number) => { + setSeccionesDeTirada(seccionesDeTirada.filter((_, i) => i !== index)); + }; + + const handleSeccionChange = (index: number, field: 'idSeccion' | 'cantPag', value: string | number) => { + const nuevasSecciones = [...seccionesDeTirada]; + const targetSeccion = nuevasSecciones[index]; + + if (field === 'idSeccion') { + const numValue = Number(value); // El valor del Select es string, pero lo guardamos como number | '' + targetSeccion.idSeccion = numValue === 0 ? '' : numValue; // Si es 0 (placeholder), guardar '' + const seccionSeleccionada = seccionesPublicacion.find(s => s.idSeccion === numValue); + targetSeccion.nombreSeccion = seccionSeleccionada?.nombre || ''; + } else { // cantPag + targetSeccion.cantPag = value as string; // Guardar como string, validar como número después + } + setSeccionesDeTirada(nuevasSecciones); + if (localErrors[`seccion_${index}_id`]) setLocalErrors(prev => ({ ...prev, [`seccion_${index}_id`]: null })); + if (localErrors[`seccion_${index}_pag`]) setLocalErrors(prev => ({ ...prev, [`seccion_${index}_pag`]: null })); + }; + + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const dataToSubmit: CreateTiradaRequestDto = { + idPublicacion: Number(idPublicacion), + fecha, + idPlanta: Number(idPlanta), + ejemplares: parseInt(ejemplares, 10), + // CORREGIDO: Asegurar que los datos de secciones sean números + secciones: seccionesDeTirada.map(s => ({ + idSeccion: Number(s.idSeccion), // Convertir a número aquí + cantPag: Number(s.cantPag) // Convertir a número aquí + })) + }; + await onSubmit(dataToSubmit); + onClose(); + } catch (error: any) { + console.error("Error en submit de TiradaFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + Registrar Nueva Tirada + + {/* ... (campos de Publicacion, Fecha, Planta, Ejemplares sin cambios) ... */} + + + Publicación + + {localErrors.idPublicacion && {localErrors.idPublicacion}} + + {setFecha(e.target.value); setLocalErrors(p => ({...p, fecha: null}));}} + margin="dense" error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading} InputLabelProps={{ shrink: true }} sx={{flex:1, minWidth: 160}} + /> + + + + Planta + + {localErrors.idPlanta && {localErrors.idPlanta}} + + {setEjemplares(e.target.value); setLocalErrors(p => ({...p, ejemplares: null}));}} + margin="dense" error={!!localErrors.ejemplares} helperText={localErrors.ejemplares || ''} + disabled={loading} sx={{flex:1, minWidth: 150}} + inputProps={{min:1}} + /> + + + + Detalle de Secciones Impresas: + {localErrors.seccionesArray && {localErrors.seccionesArray}} + {/* Permitir scroll en tabla de secciones */} + + {/* stickyHeader para que cabecera quede fija */} + + + Sección + Cant. Páginas + + + + + {seccionesDeTirada.map((sec, index) => ( + {/* Usar idTemporal para key */} + + + + {localErrors[`seccion_${index}_id`] && {localErrors[`seccion_${index}_id`]}} + + + + handleSeccionChange(index, 'cantPag', e.target.value)} + error={!!localErrors[`seccion_${index}_pag`]} + helperText={localErrors[`seccion_${index}_pag`] || ''} + disabled={loading} + inputProps={{min:1}} + /> + + + handleRemoveSeccion(index)} size="small" color="error" disabled={loading}> + + + + + ))} + {seccionesDeTirada.length === 0 && ( + + + + Agregue secciones a la tirada. + + + + )} + +
+
+ +
+ + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + +
+
+
+ ); +}; + +export default TiradaFormModal; \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaDistDto.ts b/Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaDistDto.ts new file mode 100644 index 0000000..14e844f --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaDistDto.ts @@ -0,0 +1,9 @@ +export interface CreateEntradaSalidaDistDto { + idPublicacion: number; + idDistribuidor: number; + fecha: string; // "yyyy-MM-dd" + tipoMovimiento: 'Salida' | 'Entrada'; + cantidad: number; + remito: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreatePorcMonCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/CreatePorcMonCanillaDto.ts new file mode 100644 index 0000000..e574dac --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreatePorcMonCanillaDto.ts @@ -0,0 +1,7 @@ +export interface CreatePorcMonCanillaDto { + idPublicacion: number; + idCanilla: number; + vigenciaD: string; // "yyyy-MM-dd" + porcMon: number; + esPorcentaje: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreatePorcPagoDto.ts b/Frontend/src/models/dtos/Distribucion/CreatePorcPagoDto.ts new file mode 100644 index 0000000..374f2c1 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreatePorcPagoDto.ts @@ -0,0 +1,6 @@ +export interface CreatePorcPagoDto { + idPublicacion: number; + idDistribuidor: number; + vigenciaD: string; // "yyyy-MM-dd" + porcentaje: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreatePubliSeccionDto.ts b/Frontend/src/models/dtos/Distribucion/CreatePubliSeccionDto.ts new file mode 100644 index 0000000..38d6757 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreatePubliSeccionDto.ts @@ -0,0 +1,5 @@ +export interface CreatePubliSeccionDto { + idPublicacion: number; + nombre: string; + estado?: boolean; // Default true en backend +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateRecargoZonaDto.ts b/Frontend/src/models/dtos/Distribucion/CreateRecargoZonaDto.ts new file mode 100644 index 0000000..779be88 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateRecargoZonaDto.ts @@ -0,0 +1,6 @@ +export interface CreateRecargoZonaDto { + idPublicacion: number; + idZona: number; + vigenciaD: string; // "yyyy-MM-dd" + valor: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateSalidaOtroDestinoDto.ts b/Frontend/src/models/dtos/Distribucion/CreateSalidaOtroDestinoDto.ts new file mode 100644 index 0000000..042513a --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateSalidaOtroDestinoDto.ts @@ -0,0 +1,7 @@ +export interface CreateSalidaOtroDestinoDto { + idPublicacion: number; + idDestino: number; + fecha: string; // "yyyy-MM-dd" + cantidad: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/EntradaSalidaDistDto.ts b/Frontend/src/models/dtos/Distribucion/EntradaSalidaDistDto.ts new file mode 100644 index 0000000..3f225a4 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/EntradaSalidaDistDto.ts @@ -0,0 +1,15 @@ +export interface EntradaSalidaDistDto { + idParte: number; + idPublicacion: number; + nombrePublicacion: string; + nombreEmpresaPublicacion: string; + idEmpresaPublicacion: number; + idDistribuidor: number; + nombreDistribuidor: string; + fecha: string; // "yyyy-MM-dd" + tipoMovimiento: 'Salida' | 'Entrada'; + cantidad: number; + remito: number; + observacion?: string | null; + montoCalculado: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/PorcMonCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/PorcMonCanillaDto.ts new file mode 100644 index 0000000..b932f22 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/PorcMonCanillaDto.ts @@ -0,0 +1,10 @@ +export interface PorcMonCanillaDto { + idPorcMon: number; + idPublicacion: number; + idCanilla: number; + nomApeCanilla: string; + vigenciaD: string; // "yyyy-MM-dd" + vigenciaH?: string | null; // "yyyy-MM-dd" + porcMon: number; + esPorcentaje: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/PorcPagoDto.ts b/Frontend/src/models/dtos/Distribucion/PorcPagoDto.ts new file mode 100644 index 0000000..c12e24b --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/PorcPagoDto.ts @@ -0,0 +1,9 @@ +export interface PorcPagoDto { + idPorcentaje: number; + idPublicacion: number; + idDistribuidor: number; + nombreDistribuidor: string; + vigenciaD: string; // "yyyy-MM-dd" + vigenciaH?: string | null; // "yyyy-MM-dd" + porcentaje: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/PubliSeccionDto.ts b/Frontend/src/models/dtos/Distribucion/PubliSeccionDto.ts new file mode 100644 index 0000000..546c538 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/PubliSeccionDto.ts @@ -0,0 +1,6 @@ +export interface PubliSeccionDto { + idSeccion: number; + idPublicacion: number; + nombre: string; + estado: boolean; // true = activa, false = inactiva +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/RecargoZonaDto.ts b/Frontend/src/models/dtos/Distribucion/RecargoZonaDto.ts new file mode 100644 index 0000000..db51c66 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/RecargoZonaDto.ts @@ -0,0 +1,9 @@ +export interface RecargoZonaDto { + idRecargo: number; + idPublicacion: number; + idZona: number; + nombreZona: string; + vigenciaD: string; // "yyyy-MM-dd" + vigenciaH?: string | null; // "yyyy-MM-dd" + valor: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/SalidaOtroDestinoDto.ts b/Frontend/src/models/dtos/Distribucion/SalidaOtroDestinoDto.ts new file mode 100644 index 0000000..e0d3052 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/SalidaOtroDestinoDto.ts @@ -0,0 +1,10 @@ +export interface SalidaOtroDestinoDto { + idParte: number; + idPublicacion: number; + nombrePublicacion: string; + idDestino: number; + nombreDestino: string; + fecha: string; // "yyyy-MM-dd" + cantidad: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaDistDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaDistDto.ts new file mode 100644 index 0000000..be3cb4a --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaDistDto.ts @@ -0,0 +1,4 @@ +export interface UpdateEntradaSalidaDistDto { + cantidad: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdatePorcMonCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/UpdatePorcMonCanillaDto.ts new file mode 100644 index 0000000..daed47b --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdatePorcMonCanillaDto.ts @@ -0,0 +1,5 @@ +export interface UpdatePorcMonCanillaDto { + porcMon: number; + esPorcentaje: boolean; + vigenciaH?: string | null; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdatePorcPagoDto.ts b/Frontend/src/models/dtos/Distribucion/UpdatePorcPagoDto.ts new file mode 100644 index 0000000..d2041a9 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdatePorcPagoDto.ts @@ -0,0 +1,4 @@ +export interface UpdatePorcPagoDto { + porcentaje: number; + vigenciaH?: string | null; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdatePubliSeccionDto.ts b/Frontend/src/models/dtos/Distribucion/UpdatePubliSeccionDto.ts new file mode 100644 index 0000000..983f248 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdatePubliSeccionDto.ts @@ -0,0 +1,4 @@ +export interface UpdatePubliSeccionDto { + nombre: string; + estado: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateRecargoZonaDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateRecargoZonaDto.ts new file mode 100644 index 0000000..f40b4d8 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateRecargoZonaDto.ts @@ -0,0 +1,4 @@ +export interface UpdateRecargoZonaDto { + valor: number; + vigenciaH?: string | null; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateSalidaOtroDestinoDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateSalidaOtroDestinoDto.ts new file mode 100644 index 0000000..0494104 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateSalidaOtroDestinoDto.ts @@ -0,0 +1,4 @@ +export interface UpdateSalidaOtroDestinoDto { + cantidad: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CambiarEstadoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/CambiarEstadoBobinaDto.ts new file mode 100644 index 0000000..003819d --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CambiarEstadoBobinaDto.ts @@ -0,0 +1,7 @@ +export interface CambiarEstadoBobinaDto { + nuevoEstadoId: number; + idPublicacion?: number | null; + idSeccion?: number | null; + obs?: string | null; + fechaCambioEstado: string; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CreateStockBobinaDto.ts b/Frontend/src/models/dtos/Impresion/CreateStockBobinaDto.ts new file mode 100644 index 0000000..96fb465 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CreateStockBobinaDto.ts @@ -0,0 +1,8 @@ +export interface CreateStockBobinaDto { + idTipoBobina: number; + nroBobina: string; + peso: number; + idPlanta: number; + remito: string; + fechaRemito: string; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CreateTiradaRequestDto.ts b/Frontend/src/models/dtos/Impresion/CreateTiradaRequestDto.ts new file mode 100644 index 0000000..5737519 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CreateTiradaRequestDto.ts @@ -0,0 +1,9 @@ +import type { DetalleSeccionTiradaDto } from "./DetalleSeccionTiradaDto"; + +export interface CreateTiradaRequestDto { + idPublicacion: number; + fecha: string; // "yyyy-MM-dd" + idPlanta: number; + ejemplares: number; + secciones: DetalleSeccionTiradaDto[]; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/DetalleSeccionTiradaDto.ts b/Frontend/src/models/dtos/Impresion/DetalleSeccionTiradaDto.ts new file mode 100644 index 0000000..2305099 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/DetalleSeccionTiradaDto.ts @@ -0,0 +1,4 @@ +export interface DetalleSeccionTiradaDto { + idSeccion: number; + cantPag: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/StockBobinaDto.ts b/Frontend/src/models/dtos/Impresion/StockBobinaDto.ts new file mode 100644 index 0000000..0eb22d2 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/StockBobinaDto.ts @@ -0,0 +1,19 @@ +export interface StockBobinaDto { + idBobina: number; + idTipoBobina: number; + nombreTipoBobina: string; + nroBobina: string; + peso: number; + idPlanta: number; + nombrePlanta: string; + idEstadoBobina: number; + nombreEstadoBobina: string; + remito: string; + fechaRemito: string; // "yyyy-MM-dd" + fechaEstado?: string | null; // "yyyy-MM-dd" + idPublicacion?: number | null; + nombrePublicacion?: string | null; + idSeccion?: number | null; + nombreSeccion?: string | null; + obs?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/TiradaDto.ts b/Frontend/src/models/dtos/Impresion/TiradaDto.ts new file mode 100644 index 0000000..cf785d1 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/TiradaDto.ts @@ -0,0 +1,17 @@ +export interface DetalleSeccionEnListadoDto { + idSeccion: number; + nombreSeccion: string; + cantPag: number; + idRegPublicacionSeccion: number; +} +export interface TiradaDto { + idRegistroTirada: number; + idPublicacion: number; + nombrePublicacion: string; + fecha: string; // "yyyy-MM-dd" + idPlanta: number; + nombrePlanta: string; + ejemplares: number; + seccionesImpresas: DetalleSeccionEnListadoDto[]; + totalPaginasSumadas: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/UpdateStockBobinaDto.ts b/Frontend/src/models/dtos/Impresion/UpdateStockBobinaDto.ts new file mode 100644 index 0000000..862d24c --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/UpdateStockBobinaDto.ts @@ -0,0 +1,8 @@ +export interface UpdateStockBobinaDto { + idTipoBobina: number; + nroBobina: string; + peso: number; + idPlanta: number; + remito: string; + fechaRemito: string; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx b/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx index 531295e..c4350c6 100644 --- a/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx +++ b/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx @@ -68,11 +68,6 @@ const DistribucionIndexPage: React.FC = () => { aria-label="sub-módulos de distribución" > {distribucionSubModules.map((subModule) => ( - // Usar RouterLink para que el tab se comporte como un enlace y actualice la URL - // La navegación real la manejamos con navigate en handleSubTabChange - // para poder actualizar el estado del tab seleccionado. - // Podríamos usar `component={RouterLink} to={subModule.path}` también, - // pero manejarlo con navigate da más control sobre el estado. ))} diff --git a/Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx b/Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx deleted file mode 100644 index 2a824db..0000000 --- a/Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const ESDistribuidoresPage: React.FC = () => { - return Página de Gestión de E/S de Distribuidores; -}; -export default ESDistribuidoresPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx new file mode 100644 index 0000000..f14671f --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select +} from '@mui/material'; +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 FilterListIcon from '@mui/icons-material/FilterList'; + +import entradaSalidaDistService from '../../services/Distribucion/entradaSalidaDistService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import distribuidorService from '../../services/Distribucion/distribuidorService'; + +import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto'; +import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto'; +import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; + +import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarEntradasSalidasDistPage: React.FC = () => { + const [movimientos, setMovimientos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); + const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState(''); + const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>(''); + + + const [publicaciones, setPublicaciones] = useState([]); + const [distribuidores, setDistribuidores] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingMovimiento, setEditingMovimiento] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVer = isSuperAdmin || tienePermiso("MD001"); + const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); // Para Crear, Editar, Eliminar + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const [pubsData, distData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + distribuidorService.getAllDistribuidores() + ]); + setPublicaciones(pubsData); + setDistribuidores(distData); + } catch (err) { + console.error("Error cargando datos para filtros:", err); + setError("Error al cargar opciones de filtro."); + } finally { + setLoadingFiltersDropdown(false); + } + }, []); + + useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); + + const cargarMovimientos = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + fechaDesde: filtroFechaDesde || null, + fechaHasta: filtroFechaHasta || null, + idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, + idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null, + tipoMovimiento: filtroTipoMov || null, + }; + const data = await entradaSalidaDistService.getAllEntradasSalidasDist(params); + setMovimientos(data); + } catch (err) { + console.error(err); setError('Error al cargar los movimientos.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDistribuidor, filtroTipoMov]); + + useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]); + + const handleOpenModal = (item?: EntradaSalidaDistDto) => { + setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingMovimiento(null); + }; + + const handleSubmitModal = async (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => { + setApiErrorMessage(null); + try { + if (idParte && editingMovimiento) { + await entradaSalidaDistService.updateEntradaSalidaDist(idParte, data as UpdateEntradaSalidaDistDto); + } else { + await entradaSalidaDistService.createEntradaSalidaDist(data as CreateEntradaSalidaDistDto); + } + cargarMovimientos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el movimiento.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idParte: number) => { + if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})? Esta acción revertirá el impacto en el saldo del distribuidor.`)) { + setApiErrorMessage(null); + try { + await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); + cargarMovimientos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: EntradaSalidaDistDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Entradas/Salidas Distribuidores + + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + + Publicación + + + + Distribuidor + + + + Tipo + + + + {puedeGestionar && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + FechaPublicación (Empresa) + DistribuidorTipo + CantidadRemito + Monto AfectadoObs. + {puedeGestionar && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron movimientos. + ) : ( + displayData.map((m) => ( + + {formatDate(m.fecha)} + {m.nombrePublicacion} ({m.nombreEmpresaPublicacion}) + {m.nombreDistribuidor} + + + + {m.cantidad} + {m.remito} + 0 ? 'red' : 'inherit')}}> + ${m.montoCalculado.toFixed(2)} + + {m.observacion || '-'} + {puedeGestionar && ( + + handleMenuOpen(e, m)} disabled={!puedeGestionar}> + + )} + + )))} + +
+ +
+ )} + + + {puedeGestionar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeGestionar && selectedRow && ( // O un permiso más específico si "eliminar" es diferente de "modificar" + handleDelete(selectedRow.idParte)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarEntradasSalidasDistPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarPorcMonCanillaPage.tsx b/Frontend/src/pages/Distribucion/GestionarPorcMonCanillaPage.tsx new file mode 100644 index 0000000..579fbcb --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarPorcMonCanillaPage.tsx @@ -0,0 +1,189 @@ +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, Chip +} 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 porcMonCanillaService from '../../services/Distribucion/porcMonCanillaService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import type { PorcMonCanillaDto } from '../../models/dtos/Distribucion/PorcMonCanillaDto'; +import type { CreatePorcMonCanillaDto } from '../../models/dtos/Distribucion/CreatePorcMonCanillaDto'; +import type { UpdatePorcMonCanillaDto } from '../../models/dtos/Distribucion/UpdatePorcMonCanillaDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import PorcMonCanillaFormModal from '../../components/Modals/Distribucion/PorcMonCanillaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarPorcMonCanillaPage: React.FC = () => { + const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); + const navigate = useNavigate(); + const idPublicacion = Number(idPublicacionStr); + + const [publicacion, setPublicacion] = useState(null); + const [items, setItems] = useState([]); // Renombrado de 'porcentajes' a 'items' + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [modalOpen, setModalOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Permiso CG004 para porcentajes/montos de pago de canillitas + const puedeGestionar = isSuperAdmin || tienePermiso("CG004"); + + const cargarDatos = useCallback(async () => { + if (isNaN(idPublicacion)) { + setError("ID de Publicación inválido."); setLoading(false); return; + } + if (!puedeGestionar) { + setError("No tiene permiso para gestionar esta configuración."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const [pubData, data] = await Promise.all([ + publicacionService.getPublicacionById(idPublicacion), + porcMonCanillaService.getPorcMonCanillaPorPublicacion(idPublicacion) + ]); + setPublicacion(pubData); + setItems(data); + } catch (err: any) { + console.error(err); + if (axios.isAxiosError(err) && err.response?.status === 404) { + setError(`Publicación ID ${idPublicacion} no encontrada.`); + } else { + setError('Error al cargar los datos.'); + } + } finally { setLoading(false); } + }, [idPublicacion, puedeGestionar]); + + useEffect(() => { cargarDatos(); }, [cargarDatos]); + + const handleOpenModal = (item?: PorcMonCanillaDto) => { + setEditingItem(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingItem(null); + }; + + const handleSubmitModal = async (data: CreatePorcMonCanillaDto | UpdatePorcMonCanillaDto, idPorcMon?: number) => { + setApiErrorMessage(null); + try { + if (editingItem && idPorcMon) { + await porcMonCanillaService.updatePorcMonCanilla(idPublicacion, idPorcMon, data as UpdatePorcMonCanillaDto); + } else { + await porcMonCanillaService.createPorcMonCanilla(idPublicacion, data as CreatePorcMonCanillaDto); + } + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idPorcMonDelRow: number) => { + if (window.confirm(`¿Seguro de eliminar este registro (ID: ${idPorcMonDelRow})?`)) { + setApiErrorMessage(null); + try { + await porcMonCanillaService.deletePorcMonCanilla(idPublicacion, idPorcMonDelRow); + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: PorcMonCanillaDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedRow(null); + }; + + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR') : '-'; + + if (loading) return ; + if (error) return {error}; + if (!puedeGestionar) return Acceso denegado.; + + return ( + + + Porcentajes/Montos Pago Canillita: {publicacion?.nombre || 'Cargando...'} + Empresa: {publicacion?.nombreEmpresa || '-'} + + + {puedeGestionar && ( + + )} + + + {apiErrorMessage && {apiErrorMessage}} + + + + + Canillita + Vig. Desde + Vig. Hasta + Valor + Tipo + Estado + Acciones + + + {items.length === 0 ? ( + No hay configuraciones definidas. + ) : ( + items.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nomApeCanilla.localeCompare(b.nomApeCanilla)) + .map((item) => ( + + {item.nomApeCanilla}{formatDate(item.vigenciaD)} + {formatDate(item.vigenciaH)} + {item.esPorcentaje ? `${item.porcMon.toFixed(2)}%` : `$${item.porcMon.toFixed(2)}`} + {item.esPorcentaje ? : } + {!item.vigenciaH ? : } + + handleMenuOpen(e, item)} disabled={!puedeGestionar}> + + + )))} + +
+
+ + + {puedeGestionar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Editar/Cerrar)} + {puedeGestionar && selectedRow && ( + handleDelete(selectedRow.idPorcMon)}> Eliminar)} + + + {idPublicacion && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarPorcMonCanillaPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarPorcentajesPagoPage.tsx b/Frontend/src/pages/Distribucion/GestionarPorcentajesPagoPage.tsx new file mode 100644 index 0000000..844ba54 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarPorcentajesPagoPage.tsx @@ -0,0 +1,187 @@ +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, Chip +} 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 porcPagoService from '../../services/Distribucion/porcPagoService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import type { PorcPagoDto } from '../../models/dtos/Distribucion/PorcPagoDto'; +import type { CreatePorcPagoDto } from '../../models/dtos/Distribucion/CreatePorcPagoDto'; +import type { UpdatePorcPagoDto } from '../../models/dtos/Distribucion/UpdatePorcPagoDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import PorcPagoFormModal from '../../components/Modals/Distribucion/PorcPagoFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarPorcentajesPagoPage: React.FC = () => { + const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); + const navigate = useNavigate(); + const idPublicacion = Number(idPublicacionStr); + + const [publicacion, setPublicacion] = useState(null); + const [porcentajes, setPorcentajes] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [modalOpen, setModalOpen] = useState(false); + const [editingPorcentaje, setEditingPorcentaje] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedPorcentajeRow, setSelectedPorcentajeRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Permiso DG004 para porcentajes de pago de distribuidores + const puedeGestionar = isSuperAdmin || tienePermiso("DG004"); + + const cargarDatos = useCallback(async () => { + if (isNaN(idPublicacion)) { + setError("ID de Publicación inválido."); setLoading(false); return; + } + if (!puedeGestionar) { // Permiso para ver precios de una publicacion (DP004) o el específico de porcentajes (DG004) + setError("No tiene permiso para gestionar porcentajes de pago."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const [pubData, data] = await Promise.all([ + publicacionService.getPublicacionById(idPublicacion), + porcPagoService.getPorcentajesPorPublicacion(idPublicacion) + ]); + setPublicacion(pubData); + setPorcentajes(data); + } catch (err: any) { + console.error(err); + if (axios.isAxiosError(err) && err.response?.status === 404) { + setError(`Publicación ID ${idPublicacion} no encontrada.`); + } else { + setError('Error al cargar los datos.'); + } + } finally { setLoading(false); } + }, [idPublicacion, puedeGestionar]); + + useEffect(() => { cargarDatos(); }, [cargarDatos]); + + const handleOpenModal = (item?: PorcPagoDto) => { + setEditingPorcentaje(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingPorcentaje(null); + }; + + const handleSubmitModal = async (data: CreatePorcPagoDto | UpdatePorcPagoDto, idPorcentaje?: number) => { + setApiErrorMessage(null); + try { + if (editingPorcentaje && idPorcentaje) { + await porcPagoService.updatePorcPago(idPublicacion, idPorcentaje, data as UpdatePorcPagoDto); + } else { + await porcPagoService.createPorcPago(idPublicacion, data as CreatePorcPagoDto); + } + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idPorcentajeDelRow: number) => { + if (window.confirm(`¿Seguro de eliminar este porcentaje de pago (ID: ${idPorcentajeDelRow})?`)) { + setApiErrorMessage(null); + try { + await porcPagoService.deletePorcPago(idPublicacion, idPorcentajeDelRow); + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: PorcPagoDto) => { + setAnchorEl(event.currentTarget); setSelectedPorcentajeRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedPorcentajeRow(null); + }; + + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR') : '-'; + + if (loading) return ; + if (error) return {error}; + if (!puedeGestionar) return Acceso denegado.; + + return ( + + + Porcentajes Pago Distribuidor: {publicacion?.nombre || 'Cargando...'} + Empresa: {publicacion?.nombreEmpresa || '-'} + + + {puedeGestionar && ( + + )} + + + {apiErrorMessage && {apiErrorMessage}} + + + + + Distribuidor + Vigencia Desde + Vigencia Hasta + Porcentaje (%) + Estado + Acciones + + + {porcentajes.length === 0 ? ( + No hay porcentajes definidos. + ) : ( + porcentajes.sort((a,b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreDistribuidor.localeCompare(b.nombreDistribuidor)) + .map((p) => ( + + {p.nombreDistribuidor}{formatDate(p.vigenciaD)} + {formatDate(p.vigenciaH)} + {p.porcentaje.toFixed(2)}% + {!p.vigenciaH ? : } + + handleMenuOpen(e, p)} disabled={!puedeGestionar}> + + + )))} + +
+
+ + + {puedeGestionar && selectedPorcentajeRow && ( + { handleOpenModal(selectedPorcentajeRow); handleMenuClose(); }}> Editar/Cerrar)} + {puedeGestionar && selectedPorcentajeRow && ( + handleDelete(selectedPorcentajeRow.idPorcentaje)}> Eliminar)} + + + {idPublicacion && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarPorcentajesPagoPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx b/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx index d199f73..c61f1f9 100644 --- a/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx @@ -48,8 +48,13 @@ const GestionarPublicacionesPage: React.FC = () => { const puedeModificar = isSuperAdmin || tienePermiso("DP003"); const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004"); const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005"); + const puedeGestionarPorcDist = isSuperAdmin || tienePermiso("DG004"); const puedeEliminar = isSuperAdmin || tienePermiso("DP006"); + // Permiso DP007 para secciones const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007"); + // Permiso CG004 para porcentajes/montos de pago de canillitas + const puedeGestionarPorcCan = isSuperAdmin || tienePermiso("CG004"); + const fetchEmpresas = useCallback(async () => { setLoadingEmpresas(true); @@ -149,11 +154,30 @@ const GestionarPublicacionesPage: React.FC = () => { // TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones const handleNavigateToPrecios = (idPub: number) => { + console.log("Navegando a precios para ID:", idPub); + console.log("Fila seleccionada:", selectedPublicacionRow); navigate(`/distribucion/publicaciones/${idPub}/precios`); // Ruta anidada handleMenuClose(); }; - const handleNavigateToRecargos = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/recargos`); handleMenuClose(); }; - const handleNavigateToSecciones = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/secciones`); handleMenuClose(); }; + const handleNavigateToRecargos = (idPub: number) => { + navigate(`/distribucion/publicaciones/${idPub}/recargos`); + handleMenuClose(); + }; + + const handleNavigateToPorcentajesPagoDist = (idPub: number) => { + navigate(`/distribucion/publicaciones/${idPub}/porcentajes-pago-dist`); + handleMenuClose(); + }; + + const handleNavigateToPorcMonCanilla = (idPub: number) => { + navigate(`/distribucion/publicaciones/${idPub}/porcentajes-mon-canilla`); + handleMenuClose(); + }; + + const handleNavigateToSecciones = (idPub: number) => { + navigate(`/distribucion/publicaciones/${idPub}/secciones`); + handleMenuClose(); + }; const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); @@ -241,6 +265,8 @@ const GestionarPublicacionesPage: React.FC = () => { {puedeModificar && ( { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar)} {puedeGestionarPrecios && ( handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios)} {puedeGestionarRecargos && ( handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos)} + {puedeGestionarPorcDist && ( handleNavigateToPorcentajesPagoDist(selectedPublicacionRow!.idPublicacion)}>Porcentajes Pago (Dist.))} + {puedeGestionarPorcCan && ( handleNavigateToPorcMonCanilla(selectedPublicacionRow!.idPublicacion)}>Porc./Monto Canillita)} {puedeGestionarSecciones && ( handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones)} {puedeEliminar && ( handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar)} {/* Si no hay permisos para ninguna acción */} diff --git a/Frontend/src/pages/Distribucion/GestionarRecargosPublicacionPage.tsx b/Frontend/src/pages/Distribucion/GestionarRecargosPublicacionPage.tsx new file mode 100644 index 0000000..6f24b87 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarRecargosPublicacionPage.tsx @@ -0,0 +1,197 @@ +// src/pages/Distribucion/Publicaciones/GestionarRecargosPublicacionPage.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, Chip +} 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 recargoZonaService from '../../services/Distribucion/recargoZonaService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import type { RecargoZonaDto } from '../../models/dtos/Distribucion/RecargoZonaDto'; +import type { CreateRecargoZonaDto } from '../../models/dtos/Distribucion/CreateRecargoZonaDto'; +import type { UpdateRecargoZonaDto } from '../../models/dtos/Distribucion/UpdateRecargoZonaDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import RecargoZonaFormModal from '../../components/Modals/Distribucion/RecargoZonaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarRecargosPublicacionPage: React.FC = () => { + const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); + const navigate = useNavigate(); + const idPublicacion = Number(idPublicacionStr); + + const [publicacion, setPublicacion] = useState(null); + const [recargos, setRecargos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [modalOpen, setModalOpen] = useState(false); + const [editingRecargo, setEditingRecargo] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRecargoRow, setSelectedRecargoRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005"); + + const cargarDatos = useCallback(async () => { + if (isNaN(idPublicacion)) { + setError("ID de Publicación inválido."); setLoading(false); return; + } + if (!puedeGestionarRecargos) { + setError("No tiene permiso para gestionar recargos."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const [pubData, recargosData] = await Promise.all([ + publicacionService.getPublicacionById(idPublicacion), + recargoZonaService.getRecargosPorPublicacion(idPublicacion) + ]); + setPublicacion(pubData); + setRecargos(recargosData); + } catch (err: any) { + console.error(err); + if (axios.isAxiosError(err) && err.response?.status === 404) { + setError(`Publicación ID ${idPublicacion} no encontrada o sin acceso a sus recargos.`); + } else { + setError('Error al cargar los datos de recargos.'); + } + } finally { setLoading(false); } + }, [idPublicacion, puedeGestionarRecargos]); + + useEffect(() => { cargarDatos(); }, [cargarDatos]); + + const handleOpenModal = (recargo?: RecargoZonaDto) => { + setEditingRecargo(recargo || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingRecargo(null); + }; + + const handleSubmitModal = async (data: CreateRecargoZonaDto | UpdateRecargoZonaDto, idRecargo?: number) => { + setApiErrorMessage(null); + try { + if (editingRecargo && idRecargo) { + await recargoZonaService.updateRecargoZona(idPublicacion, idRecargo, data as UpdateRecargoZonaDto); + } else { + await recargoZonaService.createRecargoZona(idPublicacion, data as CreateRecargoZonaDto); + } + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el recargo.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idRecargoDelRow: number) => { + if (window.confirm(`¿Seguro de eliminar este recargo (ID: ${idRecargoDelRow})? Puede afectar vigencias.`)) { + setApiErrorMessage(null); + try { + await recargoZonaService.deleteRecargoZona(idPublicacion, idRecargoDelRow); + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el recargo.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, recargo: RecargoZonaDto) => { + setAnchorEl(event.currentTarget); setSelectedRecargoRow(recargo); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedRecargoRow(null); + }; + + const formatDate = (dateString?: string | null) => { + if (!dateString) return '-'; + // Asume que dateString es "yyyy-MM-dd" del backend, ya formateado por el DTO. + // Si viniera como DateTime completo, necesitarías parsearlo y formatearlo. + const parts = dateString.split('-'); + if (parts.length === 3) { + return `${parts[2]}/${parts[1]}/${parts[0]}`; // dd/MM/yyyy + } + return dateString; // Devolver como está si no es el formato esperado + }; + + + if (loading) return ; + if (error) return {error}; + if (!puedeGestionarRecargos) return Acceso denegado.; + + return ( + + + Recargos por Zona para: {publicacion?.nombre || 'Cargando...'} + Empresa: {publicacion?.nombreEmpresa || '-'} + + + {puedeGestionarRecargos && ( + + )} + + + {apiErrorMessage && {apiErrorMessage}} + + + + + Zona + Vigencia Desde + Vigencia Hasta + Valor + Estado + Acciones + + + {recargos.length === 0 ? ( + No hay recargos definidos para esta publicación. + ) : ( + recargos.sort((a, b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreZona.localeCompare(b.nombreZona)) // Ordenar por fecha desc, luego zona asc + .map((r) => ( + + {r.nombreZona}{formatDate(r.vigenciaD)} + {formatDate(r.vigenciaH)} + ${r.valor.toFixed(2)} + {!r.vigenciaH ? : } + + handleMenuOpen(e, r)} disabled={!puedeGestionarRecargos}> + + + )))} + +
+
+ + + {puedeGestionarRecargos && selectedRecargoRow && ( + { handleOpenModal(selectedRecargoRow); handleMenuClose(); }}> Editar/Cerrar)} + {puedeGestionarRecargos && selectedRecargoRow && ( + handleDelete(selectedRecargoRow.idRecargo)}> Eliminar)} + + + {idPublicacion && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarRecargosPublicacionPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarSalidasOtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/GestionarSalidasOtrosDestinosPage.tsx new file mode 100644 index 0000000..7af95b9 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarSalidasOtrosDestinosPage.tsx @@ -0,0 +1,229 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select +} from '@mui/material'; +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 salidaOtroDestinoService from '../../services/Distribucion/salidaOtroDestinoService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import otroDestinoService from '../../services/Distribucion/otroDestinoService'; + +import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto'; +import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto'; +import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; + +import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarSalidasOtrosDestinosPage: React.FC = () => { + const [salidas, setSalidas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); + const [filtroIdDestino, setFiltroIdDestino] = useState(''); + + const [publicaciones, setPublicaciones] = useState([]); + const [otrosDestinos, setOtrosDestinos] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingSalida, setEditingSalida] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // SO001, SO002 (crear/modificar), SO003 (eliminar) + const puedeVer = isSuperAdmin || tienePermiso("SO001"); + const puedeCrearModificar = isSuperAdmin || tienePermiso("SO002"); + const puedeEliminar = isSuperAdmin || tienePermiso("SO003"); + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const [pubsData, destinosData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + otroDestinoService.getAllOtrosDestinos() + ]); + setPublicaciones(pubsData); + setOtrosDestinos(destinosData); + } catch (err) { + console.error("Error cargando datos para filtros:", err); + setError("Error al cargar opciones de filtro."); + } finally { + setLoadingFiltersDropdown(false); + } + }, []); + + useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); + + const cargarSalidas = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + fechaDesde: filtroFechaDesde || null, + fechaHasta: filtroFechaHasta || null, + idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, + idDestino: filtroIdDestino ? Number(filtroIdDestino) : null, + }; + const data = await salidaOtroDestinoService.getAllSalidasOtrosDestinos(params); + setSalidas(data); + } catch (err) { + console.error(err); setError('Error al cargar las salidas.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDestino]); + + useEffect(() => { cargarSalidas(); }, [cargarSalidas]); + + const handleOpenModal = (item?: SalidaOtroDestinoDto) => { + setEditingSalida(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingSalida(null); + }; + + const handleSubmitModal = async (data: CreateSalidaOtroDestinoDto | UpdateSalidaOtroDestinoDto, idParte?: number) => { + setApiErrorMessage(null); + try { + if (idParte && editingSalida) { + await salidaOtroDestinoService.updateSalidaOtroDestino(idParte, data as UpdateSalidaOtroDestinoDto); + } else { + await salidaOtroDestinoService.createSalidaOtroDestino(data as CreateSalidaOtroDestinoDto); + } + cargarSalidas(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la salida.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idParte: number) => { + if (window.confirm(`¿Seguro de eliminar este registro de salida (ID: ${idParte})?`)) { + setApiErrorMessage(null); + try { + await salidaOtroDestinoService.deleteSalidaOtroDestino(idParte); + cargarSalidas(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: SalidaOtroDestinoDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = salidas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Salidas a Otros Destinos + + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + + Publicación + + + + Destino + + + + {puedeCrearModificar && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + FechaPublicación + DestinoCantidad + Observación + {(puedeCrearModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron salidas. + ) : ( + displayData.map((s) => ( + + {formatDate(s.fecha)}{s.nombrePublicacion} + {s.nombreDestino}{s.cantidad} + {s.observacion || '-'} + {(puedeCrearModificar || puedeEliminar) && ( + + handleMenuOpen(e, s)} disabled={!puedeCrearModificar && !puedeEliminar}> + + )} + + )))} + +
+ +
+ )} + + + {puedeCrearModificar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeEliminar && selectedRow && ( + handleDelete(selectedRow.idParte)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarSalidasOtrosDestinosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarSeccionesPublicacionPage.tsx b/Frontend/src/pages/Distribucion/GestionarSeccionesPublicacionPage.tsx new file mode 100644 index 0000000..4df3e25 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarSeccionesPublicacionPage.tsx @@ -0,0 +1,186 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, Typography, Button, Paper, IconButton, Menu, MenuItem, Switch, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, + CircularProgress, Alert, Chip, FormControlLabel +} 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 publiSeccionService from '../../services/Distribucion/publiSeccionService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import type { PubliSeccionDto } from '../../models/dtos/Distribucion/PubliSeccionDto'; +import type { CreatePubliSeccionDto } from '../../models/dtos/Distribucion/CreatePubliSeccionDto'; +import type { UpdatePubliSeccionDto } from '../../models/dtos/Distribucion/UpdatePubliSeccionDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import PubliSeccionFormModal from '../../components/Modals/Distribucion/PubliSeccionFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarSeccionesPublicacionPage: React.FC = () => { + const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); + const navigate = useNavigate(); + const idPublicacion = Number(idPublicacionStr); + + const [publicacion, setPublicacion] = useState(null); + const [secciones, setSecciones] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroSoloActivas, setFiltroSoloActivas] = useState(undefined); // undefined para mostrar todas + + const [modalOpen, setModalOpen] = useState(false); + const [editingSeccion, setEditingSeccion] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedSeccionRow, setSelectedSeccionRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Permiso DP007 para gestionar secciones + const puedeGestionar = isSuperAdmin || tienePermiso("DP007"); + + const cargarDatos = useCallback(async () => { + if (isNaN(idPublicacion)) { + setError("ID de Publicación inválido."); setLoading(false); return; + } + if (!puedeGestionar) { // O también DP001 si solo quiere ver + setError("No tiene permiso para gestionar secciones."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const [pubData, data] = await Promise.all([ + publicacionService.getPublicacionById(idPublicacion), + publiSeccionService.getSeccionesPorPublicacion(idPublicacion, filtroSoloActivas) + ]); + setPublicacion(pubData); + setSecciones(data); + } catch (err: any) { + console.error(err); + if (axios.isAxiosError(err) && err.response?.status === 404) { + setError(`Publicación ID ${idPublicacion} no encontrada.`); + } else { + setError('Error al cargar los datos.'); + } + } finally { setLoading(false); } + }, [idPublicacion, puedeGestionar, filtroSoloActivas]); + + useEffect(() => { cargarDatos(); }, [cargarDatos]); + + const handleOpenModal = (item?: PubliSeccionDto) => { + setEditingSeccion(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingSeccion(null); + }; + + const handleSubmitModal = async (data: CreatePubliSeccionDto | UpdatePubliSeccionDto, idSeccion?: number) => { + setApiErrorMessage(null); + try { + if (editingSeccion && idSeccion) { + await publiSeccionService.updatePubliSeccion(idPublicacion, idSeccion, data as UpdatePubliSeccionDto); + } else { + await publiSeccionService.createPubliSeccion(idPublicacion, data as CreatePubliSeccionDto); + } + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la sección.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idSeccionDelRow: number) => { + if (window.confirm(`¿Seguro de eliminar esta sección (ID: ${idSeccionDelRow})?`)) { + setApiErrorMessage(null); + try { + await publiSeccionService.deletePubliSeccion(idPublicacion, idSeccionDelRow); + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la sección.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: PubliSeccionDto) => { + setAnchorEl(event.currentTarget); setSelectedSeccionRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedSeccionRow(null); + }; + + if (loading) return ; + if (error) return {error}; + if (!puedeGestionar) return Acceso denegado.; + + return ( + + + Secciones de: {publicacion?.nombre || 'Cargando...'} + Empresa: {publicacion?.nombreEmpresa || '-'} + + + + {puedeGestionar && ( + + )} + setFiltroSoloActivas(e.target.checked ? true : undefined)} />} + label="Mostrar solo activas" + /> + + + + {apiErrorMessage && {apiErrorMessage}} + + + + + Nombre Sección + Estado + Acciones + + + {secciones.length === 0 ? ( + No hay secciones definidas. + ) : ( + secciones.map((s) => ( + + {s.nombre} + {s.estado ? : } + + handleMenuOpen(e, s)} disabled={!puedeGestionar}> + + + )))} + +
+
+ + + {puedeGestionar && selectedSeccionRow && ( + { handleOpenModal(selectedSeccionRow); handleMenuClose(); }}> Editar)} + {puedeGestionar && selectedSeccionRow && ( + handleDelete(selectedSeccionRow.idSeccion)}> Eliminar)} + + + {idPublicacion && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarSeccionesPublicacionPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/SalidasOtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/SalidasOtrosDestinosPage.tsx deleted file mode 100644 index 5681e2f..0000000 --- a/Frontend/src/pages/Distribucion/SalidasOtrosDestinosPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const SalidastrosDestinosPage: React.FC = () => { - return Página de Gestión de Salidas a Otros Destinos; -}; -export default SalidastrosDestinosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx b/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx new file mode 100644 index 0000000..e07182a --- /dev/null +++ b/Frontend/src/pages/Impresion/GestionarStockBobinasPage.tsx @@ -0,0 +1,302 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select +} from '@mui/material'; +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 SwapHorizIcon from '@mui/icons-material/SwapHoriz'; // Para cambiar estado + +import stockBobinaService from '../../services/Impresion/stockBobinaService'; +import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; +import plantaService from '../../services/Impresion/plantaService'; +import estadoBobinaService from '../../services/Impresion/estadoBobinaService'; + +import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'; +import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto'; +import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto'; +import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto'; +import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; +import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; +import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; + +import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal'; +import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal'; +import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal'; + +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarStockBobinasPage: React.FC = () => { + const [stock, setStock] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Estados para filtros + const [filtroTipoBobina, setFiltroTipoBobina] = useState(''); + const [filtroNroBobina, setFiltroNroBobina] = useState(''); + const [filtroPlanta, setFiltroPlanta] = useState(''); + const [filtroEstadoBobina, setFiltroEstadoBobina] = useState(''); + const [filtroRemito, setFiltroRemito] = useState(''); + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + + // Datos para dropdowns de filtros + const [tiposBobina, setTiposBobina] = useState([]); + const [plantas, setPlantas] = useState([]); + const [estadosBobina, setEstadosBobina] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + + const [ingresoModalOpen, setIngresoModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false); + + const [selectedBobina, setSelectedBobina] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVer = isSuperAdmin || tienePermiso("IB001"); + const puedeIngresar = isSuperAdmin || tienePermiso("IB002"); + const puedeCambiarEstado = isSuperAdmin || tienePermiso("IB003"); + const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004"); + const puedeEliminar = isSuperAdmin || tienePermiso("IB005"); + + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const [tiposData, plantasData, estadosData] = await Promise.all([ + tipoBobinaService.getAllTiposBobina(), + plantaService.getAllPlantas(), + estadoBobinaService.getAllEstadosBobina() + ]); + setTiposBobina(tiposData); + setPlantas(plantasData); + setEstadosBobina(estadosData); + } catch (err) { + console.error("Error cargando datos para filtros:", err); + setError("Error al cargar opciones de filtro."); + } finally { + setLoadingFiltersDropdown(false); + } + }, []); + + useEffect(() => { + fetchFiltersDropdownData(); + }, [fetchFiltersDropdownData]); + + + const cargarStock = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + idTipoBobina: filtroTipoBobina ? Number(filtroTipoBobina) : null, + nroBobinaFilter: filtroNroBobina || null, + idPlanta: filtroPlanta ? Number(filtroPlanta) : null, + idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null, + remitoFilter: filtroRemito || null, + fechaDesde: filtroFechaDesde || null, + fechaHasta: filtroFechaHasta || null, + }; + const data = await stockBobinaService.getAllStockBobinas(params); + setStock(data); + } catch (err) { + console.error(err); setError('Error al cargar el stock de bobinas.'); + } finally { setLoading(false); } + }, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaDesde, filtroFechaHasta]); + + useEffect(() => { + cargarStock(); + }, [cargarStock]); + + // Handlers para modales + const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); }; + const handleCloseIngresoModal = () => setIngresoModalOpen(false); + const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => { + setApiErrorMessage(null); + try { await stockBobinaService.ingresarBobina(data); cargarStock(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; } + }; + + const handleOpenEditModal = (bobina: StockBobinaDto) => { + setSelectedBobina(bobina); setApiErrorMessage(null); setEditModalOpen(true); handleMenuClose(); + }; + const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobina(null); }; + const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => { + setApiErrorMessage(null); + try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; } + }; + + const handleOpenCambioEstadoModal = (bobina: StockBobinaDto) => { + setSelectedBobina(bobina); setApiErrorMessage(null); setCambioEstadoModalOpen(true); handleMenuClose(); + }; + const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobina(null); }; + const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => { + setApiErrorMessage(null); + try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; } + }; + + const handleDeleteBobina = async (idBobina: number) => { + if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${idBobina})? Solo se permite si está 'Disponible'.`)) { + setApiErrorMessage(null); + try { await stockBobinaService.deleteIngresoBobina(idBobina); cargarStock(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } + } + handleMenuClose(); + }; + + + const handleMenuOpen = (event: React.MouseEvent, bobina: StockBobinaDto) => { + setAnchorEl(event.currentTarget); setSelectedBobina(bobina); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedBobina(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + + if (!loading && !puedeVer) return {error || "Acceso denegado."}; + + return ( + + Stock de Bobinas + + Filtros + + + Tipo Bobina + + + setFiltroNroBobina(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/> + + Planta + + + + Estado + + + setFiltroRemito(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/> + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/> + + {/* + */} + + {puedeIngresar && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + Nro. BobinaTipoPeso (Kg) + PlantaEstadoRemito + F. RemitoF. Estado + PublicaciónSección + Obs.Acciones + + + {displayData.length === 0 ? ( + No se encontraron bobinas con los filtros aplicados. + ) : ( + displayData.map((b) => ( + + {b.nroBobina}{b.nombreTipoBobina} + {b.peso}{b.nombrePlanta} + + {b.remito}{formatDate(b.fechaRemito)} + {formatDate(b.fechaEstado)} + {b.nombrePublicacion || '-'}{b.nombreSeccion || '-'} + {b.obs || '-'} + + handleMenuOpen(e, b)} + disabled={!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar} + > + + + )))} + +
+ +
+ )} + + + {selectedBobina?.idEstadoBobina === 1 && puedeModificarDatos && ( + handleOpenEditModal(selectedBobina!)}> Editar Datos)} + {selectedBobina?.idEstadoBobina !== 3 && puedeCambiarEstado && ( // No se puede cambiar estado si está dañada + handleOpenCambioEstadoModal(selectedBobina!)}> Cambiar Estado)} + {selectedBobina?.idEstadoBobina === 1 && puedeEliminar && ( + handleDeleteBobina(selectedBobina!.idBobina)}> Eliminar Ingreso)} + {selectedBobina && selectedBobina.idEstadoBobina === 3 && (!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar) && Sin acciones} + {selectedBobina && selectedBobina.idEstadoBobina !== 1 && selectedBobina.idEstadoBobina !== 3 && (!puedeCambiarEstado) && Sin acciones} + + + + setApiErrorMessage(null)} + /> + {selectedBobina && editModalOpen && + setApiErrorMessage(null)} + /> + } + {selectedBobina && cambioEstadoModalOpen && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarStockBobinasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/GestionarTiradasPage.tsx b/Frontend/src/pages/Impresion/GestionarTiradasPage.tsx new file mode 100644 index 0000000..c0a19cc --- /dev/null +++ b/Frontend/src/pages/Impresion/GestionarTiradasPage.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, + CircularProgress, Alert, Accordion, AccordionSummary, AccordionDetails, Chip, + FormControl, + InputLabel, + Select +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import tiradaService from '../../services/Impresion/tiradaService'; +import publicacionService from '../../services/Distribucion/publicacionService'; // Para filtro +import plantaService from '../../services/Impresion/plantaService'; // Para filtro + +import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto'; +import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; + +import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarTiradasPage: React.FC = () => { + const [tiradas, setTiradas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFecha, setFiltroFecha] = useState(''); + const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); + const [filtroIdPlanta, setFiltroIdPlanta] = useState(''); + + const [publicaciones, setPublicaciones] = useState([]); + const [plantas, setPlantas] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + // No hay "editing" para tiradas por ahora, solo crear y borrar. + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVer = isSuperAdmin || tienePermiso("IT001"); + const puedeRegistrar = isSuperAdmin || tienePermiso("IT002"); + const puedeEliminar = isSuperAdmin || tienePermiso("IT003"); + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const [pubsData, plantasData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + plantaService.getAllPlantas() + ]); + setPublicaciones(pubsData); + setPlantas(plantasData); + } catch (err) { + console.error("Error cargando datos para filtros:", err); + setError("Error al cargar opciones de filtro."); + } finally { + setLoadingFiltersDropdown(false); + } + }, []); + + useEffect(() => { + fetchFiltersDropdownData(); + }, [fetchFiltersDropdownData]); + + const cargarTiradas = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + fecha: filtroFecha || null, + idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, + idPlanta: filtroIdPlanta ? Number(filtroIdPlanta) : null, + }; + const data = await tiradaService.getTiradas(params); + setTiradas(data); + } catch (err) { + console.error(err); setError('Error al cargar las tiradas.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdPlanta]); + + useEffect(() => { + cargarTiradas(); + }, [cargarTiradas]); + + const handleOpenModal = () => { setApiErrorMessage(null); setModalOpen(true); }; + const handleCloseModal = () => setModalOpen(false); + + const handleSubmitModal = async (data: CreateTiradaRequestDto) => { + setApiErrorMessage(null); + try { + await tiradaService.registrarTirada(data); + cargarTiradas(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al registrar la tirada.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDeleteTirada = async (tirada: TiradaDto) => { + if (window.confirm(`¿Seguro de eliminar la tirada del ${tirada.fecha} para "${tirada.nombrePublicacion}" en planta "${tirada.nombrePlanta}"? Esta acción eliminará el total de ejemplares y todas sus secciones asociadas.`)) { + setApiErrorMessage(null); + try { + await tiradaService.deleteTiradaCompleta(tirada.fecha, tirada.idPublicacion, tirada.idPlanta); + cargarTiradas(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la tirada.'; + setApiErrorMessage(message); + } + } + }; + + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Gestión de Tiradas + + Filtros + + setFiltroFecha(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + + Publicación + + + + Planta + + + {/* */} + + {puedeRegistrar && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + {tiradas.length === 0 ? ( + No se encontraron tiradas con los filtros aplicados. + ) : ( + tiradas.map((tirada) => ( + + }> + + {formatDate(tirada.fecha)} - {tirada.nombrePublicacion} ({tirada.nombrePlanta}) + + + + {puedeEliminar && ( + { e.stopPropagation(); handleDeleteTirada(tirada);}} sx={{ml:1}}> + + + )} + + + + + + + + Sección + Páginas + + + {tirada.seccionesImpresas.map(sec => ( + + {sec.nombreSeccion} + {sec.cantPag} + + ))} + +
+
+
+
+ )) + )} +
+ )} + {/* No hay paginación para la lista de Acordeones por ahora */} + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarTiradasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx b/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx index 38d083e..6c19b30 100644 --- a/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx +++ b/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx @@ -7,8 +7,8 @@ const impresionSubModules = [ { label: 'Plantas', path: 'plantas' }, { label: 'Tipos Bobina', path: 'tipos-bobina' }, { label: 'Estados Bobina', path: 'estados-bobina' }, - // { label: 'Stock Bobinas', path: 'stock-bobinas' }, - // { label: 'Tiradas', path: 'tiradas' }, + { label: 'Stock Bobinas', path: 'stock-bobinas' }, + { label: 'Tiradas', path: 'tiradas' }, ]; const ImpresionIndexPage: React.FC = () => { diff --git a/Frontend/src/routes/AppRoutes.tsx b/Frontend/src/routes/AppRoutes.tsx index 5a9d6e3..306a2e9 100644 --- a/Frontend/src/routes/AppRoutes.tsx +++ b/Frontend/src/routes/AppRoutes.tsx @@ -11,21 +11,27 @@ import { Typography } from '@mui/material'; import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage'; import ESCanillasPage from '../pages/Distribucion/ESCanillasPage'; import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage'; -import ESDistribuidoresPage from '../pages/Distribucion/ESDistribuidoresPage'; -import SalidasOtrosDestinosPage from '../pages/Distribucion/SalidasOtrosDestinosPage'; import GestionarCanillitasPage from '../pages/Distribucion/GestionarCanillitasPage'; import GestionarDistribuidoresPage from '../pages/Distribucion/GestionarDistribuidoresPage'; -import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage'; // Ajusta la ruta si la moviste +import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage'; +import GestionarSeccionesPublicacionPage from '../pages/Distribucion/GestionarSeccionesPublicacionPage'; import GestionarPreciosPublicacionPage from '../pages/Distribucion/GestionarPreciosPublicacionPage'; +import GestionarRecargosPublicacionPage from '../pages/Distribucion/GestionarRecargosPublicacionPage'; +import GestionarPorcentajesPagoPage from '../pages/Distribucion/GestionarPorcentajesPagoPage'; +import GestionarPorcMonCanillaPage from '../pages/Distribucion/GestionarPorcMonCanillaPage'; import GestionarOtrosDestinosPage from '../pages/Distribucion/GestionarOtrosDestinosPage'; import GestionarZonasPage from '../pages/Distribucion/GestionarZonasPage'; import GestionarEmpresasPage from '../pages/Distribucion/GestionarEmpresasPage'; +import GestionarSalidasOtrosDestinosPage from '../pages/Distribucion/GestionarSalidasOtrosDestinosPage'; +import GestionarEntradasSalidasDistPage from '../pages/Distribucion/GestionarEntradasSalidasDistPage'; // Impresión import ImpresionIndexPage from '../pages/Impresion/ImpresionIndexPage'; import GestionarPlantasPage from '../pages/Impresion/GestionarPlantasPage'; import GestionarTiposBobinaPage from '../pages/Impresion/GestionarTiposBobinaPage'; import GestionarEstadosBobinaPage from '../pages/Impresion/GestionarEstadosBobinaPage'; +import GestionarStockBobinasPage from '../pages/Impresion/GestionarStockBobinasPage'; +import GestionarTiradasPage from '../pages/Impresion/GestionarTiradasPage'; // Contables import ContablesIndexPage from '../pages/Contables/ContablesIndexPage'; @@ -97,15 +103,22 @@ const AppRoutes = () => { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> - } /> - } /> } /> } /> - } /> + } /> + {/* Rutas para Publicaciones y sus detalles */} + }> {/* Contenedor para sub-rutas de publicaciones */} + } /> {/* Lista de publicaciones */} + } /> + } /> + } /> + } /> + } /> + {/* Módulo Contable (anidado) */} @@ -121,10 +134,11 @@ const AppRoutes = () => { } /> } /> } /> + } /> + } /> {/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */} - } /> } /> } /> diff --git a/Frontend/src/services/Distribucion/entradaSalidaDistService.ts b/Frontend/src/services/Distribucion/entradaSalidaDistService.ts new file mode 100644 index 0000000..e46753d --- /dev/null +++ b/Frontend/src/services/Distribucion/entradaSalidaDistService.ts @@ -0,0 +1,52 @@ +import apiClient from '../apiClient'; +import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto'; +import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto'; +import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto'; + +interface GetAllESDistParams { + fechaDesde?: string | null; // yyyy-MM-dd + fechaHasta?: string | null; // yyyy-MM-dd + idPublicacion?: number | null; + idDistribuidor?: number | null; + tipoMovimiento?: 'Salida' | 'Entrada' | '' | null; +} + +const getAllEntradasSalidasDist = async (filters: GetAllESDistParams): Promise => { + const params: Record = {}; + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion; + if (filters.idDistribuidor) params.idDistribuidor = filters.idDistribuidor; + if (filters.tipoMovimiento) params.tipoMovimiento = filters.tipoMovimiento; + + const response = await apiClient.get('/entradassalidasdist', { params }); + return response.data; +}; + +const getEntradaSalidaDistById = async (idParte: number): Promise => { + const response = await apiClient.get(`/entradassalidasdist/${idParte}`); + return response.data; +}; + +const createEntradaSalidaDist = async (data: CreateEntradaSalidaDistDto): Promise => { + const response = await apiClient.post('/entradassalidasdist', data); + return response.data; +}; + +const updateEntradaSalidaDist = async (idParte: number, data: UpdateEntradaSalidaDistDto): Promise => { + await apiClient.put(`/entradassalidasdist/${idParte}`, data); +}; + +const deleteEntradaSalidaDist = async (idParte: number): Promise => { + await apiClient.delete(`/entradassalidasdist/${idParte}`); +}; + +const entradaSalidaDistService = { + getAllEntradasSalidasDist, + getEntradaSalidaDistById, + createEntradaSalidaDist, + updateEntradaSalidaDist, + deleteEntradaSalidaDist, +}; + +export default entradaSalidaDistService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/porcMonCanillaService.ts b/Frontend/src/services/Distribucion/porcMonCanillaService.ts new file mode 100644 index 0000000..bf45aaa --- /dev/null +++ b/Frontend/src/services/Distribucion/porcMonCanillaService.ts @@ -0,0 +1,37 @@ +import apiClient from '../apiClient'; +import type { PorcMonCanillaDto } from '../../models/dtos/Distribucion/PorcMonCanillaDto'; +import type { CreatePorcMonCanillaDto } from '../../models/dtos/Distribucion/CreatePorcMonCanillaDto'; +import type { UpdatePorcMonCanillaDto } from '../../models/dtos/Distribucion/UpdatePorcMonCanillaDto'; + +const getPorcMonCanillaPorPublicacion = async (idPublicacion: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/porcentajesmoncanilla`); + return response.data; +}; + +const getPorcMonCanillaById = async (idPublicacion: number, idPorcMon: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/porcentajesmoncanilla/${idPorcMon}`); + return response.data; +}; + +const createPorcMonCanilla = async (idPublicacion: number, data: CreatePorcMonCanillaDto): Promise => { + const response = await apiClient.post(`/publicaciones/${idPublicacion}/porcentajesmoncanilla`, data); + return response.data; +}; + +const updatePorcMonCanilla = async (idPublicacion: number, idPorcMon: number, data: UpdatePorcMonCanillaDto): Promise => { + await apiClient.put(`/publicaciones/${idPublicacion}/porcentajesmoncanilla/${idPorcMon}`, data); +}; + +const deletePorcMonCanilla = async (idPublicacion: number, idPorcMon: number): Promise => { + await apiClient.delete(`/publicaciones/${idPublicacion}/porcentajesmoncanilla/${idPorcMon}`); +}; + +const porcMonCanillaService = { + getPorcMonCanillaPorPublicacion, + getPorcMonCanillaById, + createPorcMonCanilla, + updatePorcMonCanilla, + deletePorcMonCanilla, +}; + +export default porcMonCanillaService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/porcPagoService.ts b/Frontend/src/services/Distribucion/porcPagoService.ts new file mode 100644 index 0000000..7faeb17 --- /dev/null +++ b/Frontend/src/services/Distribucion/porcPagoService.ts @@ -0,0 +1,39 @@ +import apiClient from '../apiClient'; +import type { PorcPagoDto } from '../../models/dtos/Distribucion/PorcPagoDto'; +import type { CreatePorcPagoDto } from '../../models/dtos/Distribucion/CreatePorcPagoDto'; +import type { UpdatePorcPagoDto } from '../../models/dtos/Distribucion/UpdatePorcPagoDto'; + +const getPorcentajesPorPublicacion = async (idPublicacion: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/porcentajespago`); + return response.data; +}; + +// getPorcPagoById no es estrictamente necesario para el CRUD dentro de la página de una publicación, +// pero podría ser útil para una edición muy específica o si se accede directamente. +const getPorcPagoById = async (idPublicacion: number, idPorcentaje: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/porcentajespago/${idPorcentaje}`); + return response.data; +}; + +const createPorcPago = async (idPublicacion: number, data: CreatePorcPagoDto): Promise => { + const response = await apiClient.post(`/publicaciones/${idPublicacion}/porcentajespago`, data); + return response.data; +}; + +const updatePorcPago = async (idPublicacion: number, idPorcentaje: number, data: UpdatePorcPagoDto): Promise => { + await apiClient.put(`/publicaciones/${idPublicacion}/porcentajespago/${idPorcentaje}`, data); +}; + +const deletePorcPago = async (idPublicacion: number, idPorcentaje: number): Promise => { + await apiClient.delete(`/publicaciones/${idPublicacion}/porcentajespago/${idPorcentaje}`); +}; + +const porcPagoService = { + getPorcentajesPorPublicacion, + getPorcPagoById, + createPorcPago, + updatePorcPago, + deletePorcPago, +}; + +export default porcPagoService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/publiSeccionService.ts b/Frontend/src/services/Distribucion/publiSeccionService.ts new file mode 100644 index 0000000..7523d0e --- /dev/null +++ b/Frontend/src/services/Distribucion/publiSeccionService.ts @@ -0,0 +1,41 @@ +import apiClient from '../apiClient'; +import type { PubliSeccionDto } from '../../models/dtos/Distribucion/PubliSeccionDto'; +import type { CreatePubliSeccionDto } from '../../models/dtos/Distribucion/CreatePubliSeccionDto'; +import type { UpdatePubliSeccionDto } from '../../models/dtos/Distribucion/UpdatePubliSeccionDto'; + +const getSeccionesPorPublicacion = async (idPublicacion: number, soloActivas?: boolean): Promise => { + const params: Record = {}; + if (soloActivas !== undefined) { + params.soloActivas = soloActivas; + } + const response = await apiClient.get(`/publicaciones/${idPublicacion}/secciones`, { params }); + return response.data; +}; + +const getPubliSeccionById = async (idPublicacion: number, idSeccion: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/secciones/${idSeccion}`); + return response.data; +}; + +const createPubliSeccion = async (idPublicacion: number, data: CreatePubliSeccionDto): Promise => { + const response = await apiClient.post(`/publicaciones/${idPublicacion}/secciones`, data); + return response.data; +}; + +const updatePubliSeccion = async (idPublicacion: number, idSeccion: number, data: UpdatePubliSeccionDto): Promise => { + await apiClient.put(`/publicaciones/${idPublicacion}/secciones/${idSeccion}`, data); +}; + +const deletePubliSeccion = async (idPublicacion: number, idSeccion: number): Promise => { + await apiClient.delete(`/publicaciones/${idPublicacion}/secciones/${idSeccion}`); +}; + +const publiSeccionService = { + getSeccionesPorPublicacion, + getPubliSeccionById, + createPubliSeccion, + updatePubliSeccion, + deletePubliSeccion, +}; + +export default publiSeccionService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/recargoZonaService.ts b/Frontend/src/services/Distribucion/recargoZonaService.ts new file mode 100644 index 0000000..c464ecc --- /dev/null +++ b/Frontend/src/services/Distribucion/recargoZonaService.ts @@ -0,0 +1,38 @@ +// src/services/recargoZonaService.ts +import apiClient from '../apiClient'; +import type { RecargoZonaDto } from '../../models/dtos/Distribucion/RecargoZonaDto'; +import type { CreateRecargoZonaDto } from '../../models/dtos/Distribucion/CreateRecargoZonaDto'; +import type { UpdateRecargoZonaDto } from '../../models/dtos/Distribucion/UpdateRecargoZonaDto'; + +const getRecargosPorPublicacion = async (idPublicacion: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/recargos`); + return response.data; +}; + +const getRecargoZonaById = async (idPublicacion: number, idRecargo: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/recargos/${idRecargo}`); + return response.data; +}; + +const createRecargoZona = async (idPublicacion: number, data: CreateRecargoZonaDto): Promise => { + const response = await apiClient.post(`/publicaciones/${idPublicacion}/recargos`, data); + return response.data; +}; + +const updateRecargoZona = async (idPublicacion: number, idRecargo: number, data: UpdateRecargoZonaDto): Promise => { + await apiClient.put(`/publicaciones/${idPublicacion}/recargos/${idRecargo}`, data); +}; + +const deleteRecargoZona = async (idPublicacion: number, idRecargo: number): Promise => { + await apiClient.delete(`/publicaciones/${idPublicacion}/recargos/${idRecargo}`); +}; + +const recargoZonaService = { + getRecargosPorPublicacion, + getRecargoZonaById, + createRecargoZona, + updateRecargoZona, + deleteRecargoZona, +}; + +export default recargoZonaService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/salidaOtroDestinoService.ts b/Frontend/src/services/Distribucion/salidaOtroDestinoService.ts new file mode 100644 index 0000000..d562299 --- /dev/null +++ b/Frontend/src/services/Distribucion/salidaOtroDestinoService.ts @@ -0,0 +1,50 @@ +import apiClient from '../apiClient'; +import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto'; +import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto'; +import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto'; + +interface GetAllSalidasParams { + fechaDesde?: string | null; // yyyy-MM-dd + fechaHasta?: string | null; // yyyy-MM-dd + idPublicacion?: number | null; + idDestino?: number | null; +} + +const getAllSalidasOtrosDestinos = async (filters: GetAllSalidasParams): Promise => { + const params: Record = {}; + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion; + if (filters.idDestino) params.idDestino = filters.idDestino; + + const response = await apiClient.get('/salidasotrosdestinos', { params }); + return response.data; +}; + +const getSalidaOtroDestinoById = async (idParte: number): Promise => { + const response = await apiClient.get(`/salidasotrosdestinos/${idParte}`); + return response.data; +}; + +const createSalidaOtroDestino = async (data: CreateSalidaOtroDestinoDto): Promise => { + const response = await apiClient.post('/salidasotrosdestinos', data); + return response.data; +}; + +const updateSalidaOtroDestino = async (idParte: number, data: UpdateSalidaOtroDestinoDto): Promise => { + await apiClient.put(`/salidasotrosdestinos/${idParte}`, data); +}; + +const deleteSalidaOtroDestino = async (idParte: number): Promise => { + await apiClient.delete(`/salidasotrosdestinos/${idParte}`); +}; + +const salidaOtroDestinoService = { + getAllSalidasOtrosDestinos, + getSalidaOtroDestinoById, + createSalidaOtroDestino, + updateSalidaOtroDestino, + deleteSalidaOtroDestino, +}; + +export default salidaOtroDestinoService; \ No newline at end of file diff --git a/Frontend/src/services/Impresion/stockBobinaService.ts b/Frontend/src/services/Impresion/stockBobinaService.ts new file mode 100644 index 0000000..e424261 --- /dev/null +++ b/Frontend/src/services/Impresion/stockBobinaService.ts @@ -0,0 +1,62 @@ +import apiClient from '../apiClient'; +import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'; +import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto'; +import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto'; +import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto'; + +interface GetAllStockBobinasParams { + idTipoBobina?: number | null; + nroBobinaFilter?: string | null; + idPlanta?: number | null; + idEstadoBobina?: number | null; + remitoFilter?: string | null; + fechaDesde?: string | null; // "yyyy-MM-dd" + fechaHasta?: string | null; // "yyyy-MM-dd" +} + +const getAllStockBobinas = async (filters: GetAllStockBobinasParams): Promise => { + const params: Record = {}; + if (filters.idTipoBobina) params.idTipoBobina = filters.idTipoBobina; + if (filters.nroBobinaFilter) params.nroBobina = filters.nroBobinaFilter; // El backend espera nroBobina + if (filters.idPlanta) params.idPlanta = filters.idPlanta; + if (filters.idEstadoBobina) params.idEstadoBobina = filters.idEstadoBobina; + if (filters.remitoFilter) params.remito = filters.remitoFilter; // El backend espera remito + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + + const response = await apiClient.get('/stockbobinas', { params }); + return response.data; +}; + +const getStockBobinaById = async (idBobina: number): Promise => { + const response = await apiClient.get(`/stockbobinas/${idBobina}`); + return response.data; +}; + +const ingresarBobina = async (data: CreateStockBobinaDto): Promise => { + const response = await apiClient.post('/stockbobinas', data); + return response.data; +}; + +const updateDatosBobinaDisponible = async (idBobina: number, data: UpdateStockBobinaDto): Promise => { + await apiClient.put(`/stockbobinas/${idBobina}/datos`, data); +}; + +const cambiarEstadoBobina = async (idBobina: number, data: CambiarEstadoBobinaDto): Promise => { + await apiClient.put(`/stockbobinas/${idBobina}/cambiar-estado`, data); +}; + +const deleteIngresoBobina = async (idBobina: number): Promise => { + await apiClient.delete(`/stockbobinas/${idBobina}`); +}; + +const stockBobinaService = { + getAllStockBobinas, + getStockBobinaById, + ingresarBobina, + updateDatosBobinaDisponible, + cambiarEstadoBobina, + deleteIngresoBobina, +}; + +export default stockBobinaService; \ No newline at end of file diff --git a/Frontend/src/services/Impresion/tiradaService.ts b/Frontend/src/services/Impresion/tiradaService.ts new file mode 100644 index 0000000..ced7b4b --- /dev/null +++ b/Frontend/src/services/Impresion/tiradaService.ts @@ -0,0 +1,43 @@ +import apiClient from '../apiClient'; +import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto'; +import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto'; + +interface GetTiradasParams { + fecha?: string | null; // "yyyy-MM-dd" + idPublicacion?: number | null; + idPlanta?: number | null; +} + +const getTiradas = async (filters: GetTiradasParams): Promise => { + const params: Record = {}; + if (filters.fecha) params.fecha = filters.fecha; + if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion; + if (filters.idPlanta) params.idPlanta = filters.idPlanta; + + const response = await apiClient.get('/tiradas', { params }); + return response.data; +}; + +const registrarTirada = async (data: CreateTiradaRequestDto): Promise => { + const response = await apiClient.post('/tiradas', data); + return response.data; // El backend devuelve la tirada creada +}; + +const deleteTiradaCompleta = async (fecha: string, idPublicacion: number, idPlanta: number): Promise => { + // Los parámetros van en la query string para este DELETE + await apiClient.delete('/tiradas', { + params: { + fecha, + idPublicacion, + idPlanta + } + }); +}; + +const tiradaService = { + getTiradas, + registrarTirada, + deleteTiradaCompleta, +}; + +export default tiradaService; \ No newline at end of file