feat: Implementación de Secciones, Recargos, Porc. Pago Dist. y backend E/S Dist.

Backend API:
- Recargos por Zona (`dist_RecargoZona`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/recargos`.
  - Lógica de negocio para vigencias (cierre/reapertura de períodos).
  - Auditoría en `dist_RecargoZona_H`.
- Porcentajes de Pago Distribuidores (`dist_PorcPago`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajespago`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcPago_H`.
- Porcentajes/Montos Pago Canillitas (`dist_PorcMonPagoCanilla`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajesmoncanilla`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcMonPagoCanilla_H`.
- Secciones de Publicación (`dist_dtPubliSecciones`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/secciones`.
  - Auditoría en `dist_dtPubliSecciones_H`.
- Entradas/Salidas Distribuidores (`dist_EntradasSalidas`):
  - Implementado backend (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para determinar precios/recargos/porcentajes aplicables.
  - Cálculo de monto y afectación de saldos de distribuidores en `cue_Saldos`.
  - Auditoría en `dist_EntradasSalidas_H`.
- Correcciones de Mapeo Dapper:
  - Aplicados alias explícitos en repositorios de RecargoZona, PorcPago, PorcMonCanilla, PubliSeccion,
    Canilla, Distribuidor y Precio para asegurar mapeo correcto de IDs y columnas.

Frontend React:
- Recargos por Zona:
  - `recargoZonaService.ts`.
  - `RecargoZonaFormModal.tsx` para crear/editar períodos de recargos.
  - `GestionarRecargosPublicacionPage.tsx` para listar y gestionar recargos por publicación.
- Porcentajes de Pago Distribuidores:
  - `porcPagoService.ts`.
  - `PorcPagoFormModal.tsx`.
  - `GestionarPorcentajesPagoPage.tsx`.
- Porcentajes/Montos Pago Canillitas:
  - `porcMonCanillaService.ts`.
  - `PorcMonCanillaFormModal.tsx`.
  - `GestionarPorcMonCanillaPage.tsx`.
- Secciones de Publicación:
  - `publiSeccionService.ts`.
  - `PubliSeccionFormModal.tsx`.
  - `GestionarSeccionesPublicacionPage.tsx`.
- Navegación:
  - Actualizadas rutas y menús para acceder a la gestión de recargos, porcentajes (dist. y canillita) y secciones desde la vista de una publicación.
- Layout:
  - Uso consistente de `Box` con Flexbox en lugar de `Grid` en nuevos modales y páginas para evitar errores de tipo.
This commit is contained in:
2025-05-21 14:58:52 -03:00
parent b6ba52f074
commit e7e185a9cb
140 changed files with 10465 additions and 394 deletions

View File

@@ -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<EntradasSalidasDistController> _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<EntradasSalidasDistController> 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<EntradaSalidaDistDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<PorcentajesMonCanillaController> _logger;
// Permiso CG004 para porcentajes de pago de canillitas
private const string PermisoGestionar = "CG004";
public PorcentajesMonCanillaController(IPorcMonCanillaService porcMonCanillaService, ILogger<PorcentajesMonCanillaController> 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<PorcMonCanillaDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<PorcentajesPagoController> _logger;
// Permiso DG004 para gestionar porcentajes de pago de distribuidores
private const string PermisoGestionarPorcentajes = "DG004";
public PorcentajesPagoController(IPorcPagoService porcPagoService, ILogger<PorcentajesPagoController> 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<PorcPagoDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<PubliSeccionesController> _logger;
// Permiso DP007 para gestionar secciones de publicaciones
private const string PermisoGestionarSecciones = "DP007";
public PubliSeccionesController(IPubliSeccionService publiSeccionService, ILogger<PubliSeccionesController> 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<PubliSeccionDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<RecargosZonaController> _logger;
// Permiso DP005 para gestionar recargos
private const string PermisoGestionarRecargos = "DP005";
public RecargosZonaController(IRecargoZonaService recargoZonaService, ILogger<RecargosZonaController> 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<RecargoZonaDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<SalidasOtrosDestinosController> _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<SalidasOtrosDestinosController> 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<SalidaOtroDestinoDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<StockBobinasController> _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<StockBobinasController> 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<StockBobinaDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<TiradasController> _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<TiradasController> 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<TiradaDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}

View File

@@ -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<IEnumerable<(Canilla Canilla, string NombreZona, string NombreEmpresa)>> 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<Canilla, string, string, (Canilla, string, string)>(
var result = await connection.QueryAsync<Canilla, string, string, (Canilla?, string?, string?)>(
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<Canilla?> GetByIdSimpleAsync(int id) // Para uso interno del servicio
public async Task<Canilla?> GetByIdSimpleAsync(int id)
{
const string sql = "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @Id";
using var connection = _connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Canilla>(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<Canilla>(sql, new { IdParam = id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener Canilla (simple) por ID: {IdCanilla}", id);
return null;
}
}
public async Task<bool> 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<Canilla>(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<Canilla>(
"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<Canilla>(
"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;
}
}

View File

@@ -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<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> 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<Distribuidor, string, (Distribuidor, string?)>(
var result = await connection.QueryAsync<Distribuidor, string, (Distribuidor?, string?)>(
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<Distribuidor?> GetByIdSimpleAsync(int id)
{
const string sql = "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id";
using var connection = _connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Distribuidor>(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<Distribuidor>(sql, new { IdParam = id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener Distribuidor (simple) por ID: {IdDistribuidor}", id);
return null;
}
}
public async Task<bool> 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<bool>(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<bool>(sqlBuilder.ToString(), parameters);
}
public async Task<bool> 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<bool>(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<bool>(sqlBuilder.ToString(), parameters);
}
public async Task<bool> 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<int?>(query, new { Id = id }) == 1) return true;
foreach (var query in checkQueries)
{
if (await connection.ExecuteScalarAsync<int?>(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<Distribuidor?> 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<Distribuidor>(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<Distribuidor>(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<Distribuidor>(
"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<Distribuidor>(
"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;
}
}

View File

@@ -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<EntradaSalidaDistRepository> _log;
public EntradaSalidaDistRepository(DbConnectionFactory cf, ILogger<EntradaSalidaDistRepository> log)
{
_cf = cf;
_log = log;
}
public async Task<IEnumerable<EntradaSalidaDist>> 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<EntradaSalidaDist>(sqlBuilder.ToString(), parameters);
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener Entradas/Salidas de Distribuidores.");
return Enumerable.Empty<EntradaSalidaDist>();
}
}
public async Task<EntradaSalidaDist?> 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<EntradaSalidaDist>(sql, new { IdParteParam = idParte });
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener EntradaSalidaDist por ID: {IdParte}", idParte);
return null;
}
}
public async Task<bool> 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<bool>(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<EntradaSalidaDist?> 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<EntradaSalidaDist>(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<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<EntradaSalidaDist>(
@"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<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<EntradaSalidaDist>(
@"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;
}
}
}

View File

@@ -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<IEnumerable<EntradaSalidaDist>> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento);
Task<EntradaSalidaDist?> GetByIdAsync(int idParte);
Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction);
Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null);
}
}

View File

@@ -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<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction);
}
public interface IPorcMonCanillaRepository
{
Task<IEnumerable<(PorcMonCanilla Item, string NomApeCanilla)>> GetByPublicacionIdAsync(int idPublicacion);
Task<PorcMonCanilla?> GetByIdAsync(int idPorcMon);
Task<PorcMonCanilla?> GetActiveByPublicacionCanillaAndDateAsync(int idPublicacion, int idCanilla, DateTime fecha, IDbTransaction? transaction = null);
Task<PorcMonCanilla?> CreateAsync(PorcMonCanilla nuevoItem, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(PorcMonCanilla itemAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idPorcMon, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction);
Task<PorcMonCanilla?> GetPreviousActiveAsync(int idPublicacion, int idCanilla, DateTime vigenciaDNuevo, IDbTransaction transaction);
}
}

View File

@@ -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<IEnumerable<(PorcPago PorcPago, string NombreDistribuidor)>> GetByPublicacionIdAsync(int idPublicacion);
Task<PorcPago?> GetByIdAsync(int idPorcentaje);
Task<PorcPago?> GetActiveByPublicacionDistribuidorAndDateAsync(int idPublicacion, int idDistribuidor, DateTime fecha, IDbTransaction? transaction = null);
Task<PorcPago?> CreateAsync(PorcPago nuevoPorcPago, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(PorcPago porcPagoAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idPorcentaje, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction);
Task<PorcPago?> GetPreviousActivePorcPagoAsync(int idPublicacion, int idDistribuidor, DateTime vigenciaDNuevo, IDbTransaction transaction);
}
}

View File

@@ -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<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction);
}
public interface IPubliSeccionRepository
{
Task<IEnumerable<PubliSeccion>> GetByPublicacionIdAsync(int idPublicacion, bool? soloActivas = null);
Task<PubliSeccion?> GetByIdAsync(int idSeccion);
Task<PubliSeccion?> CreateAsync(PubliSeccion nuevaSeccion, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(PubliSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); // Ya existe
Task<bool> ExistsByNameInPublicacionAsync(string nombre, int idPublicacion, int? excludeIdSeccion = null);
Task<bool> IsInUseAsync(int idSeccion); // Verificar en bob_RegPublicaciones, bob_StockBobinas
}
}

View File

@@ -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<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction);
Task<IEnumerable<(RecargoZona Recargo, string NombreZona)>> GetByPublicacionIdAsync(int idPublicacion);
Task<RecargoZona?> GetByIdAsync(int idRecargo); // Para obtener un recargo específico
Task<RecargoZona?> GetActiveByPublicacionZonaAndDateAsync(int idPublicacion, int idZona, DateTime fecha, IDbTransaction? transaction = null);
Task<RecargoZona?> CreateAsync(RecargoZona nuevoRecargo, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(RecargoZona recargoAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idRecargo, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); // Ya existe
Task<RecargoZona?> GetPreviousActiveRecargoAsync(int idPublicacion, int idZona, DateTime vigenciaDNuevo, IDbTransaction transaction);
}
}

View File

@@ -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<IEnumerable<SalidaOtroDestino>> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino);
Task<SalidaOtroDestino?> GetByIdAsync(int idParte);
Task<SalidaOtroDestino?> CreateAsync(SalidaOtroDestino nuevaSalida, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(SalidaOtroDestino salidaAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction);
}
}

View File

@@ -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<PorcMonCanillaRepository> _log;
public PorcMonCanillaRepository(DbConnectionFactory cf, ILogger<PorcMonCanillaRepository> log) { _cf = cf; _log = log; }
private readonly DbConnectionFactory _cf;
private readonly ILogger<PorcMonCanillaRepository> _log;
public PorcMonCanillaRepository(DbConnectionFactory connectionFactory, ILogger<PorcMonCanillaRepository> logger)
{
_cf = connectionFactory;
_log = logger;
}
public async Task<IEnumerable<(PorcMonCanilla Item, string NomApeCanilla)>> 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<PorcMonCanilla, string, (PorcMonCanilla, string)>(
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<PorcMonCanilla?> 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<PorcMonCanilla>(sql, new { IdPorcMonParam = idPorcMon });
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener PorcMonCanilla por ID: {IdPorcMon}", idPorcMon);
return null;
}
}
public async Task<PorcMonCanilla?> 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<PorcMonCanilla>(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<PorcMonCanilla?> 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<PorcMonCanilla>(sql,
new { IdPublicacionParam = idPublicacion, IdCanillaParam = idCanilla, VigenciaDNuevoParam = vigenciaDNuevo.Date },
transaction);
}
public async Task<PorcMonCanilla?> 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<PorcMonCanilla>(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<bool> UpdateAsync(PorcMonCanilla itemAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PorcMonCanilla>(
@"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<bool> DeleteAsync(int idPorcMon, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PorcMonCanilla>(
@"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<bool> 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<PorcMonCanilla>(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<PorcMonCanilla>(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;
}
}

View File

@@ -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<PorcPagoRepository> _log;
public PorcPagoRepository(DbConnectionFactory cf, ILogger<PorcPagoRepository> log) { _cf = cf; _log = log; }
private readonly DbConnectionFactory _cf; // Renombrado para brevedad
private readonly ILogger<PorcPagoRepository> _log; // Renombrado para brevedad
public PorcPagoRepository(DbConnectionFactory connectionFactory, ILogger<PorcPagoRepository> logger)
{
_cf = connectionFactory;
_log = logger;
}
public async Task<IEnumerable<(PorcPago PorcPago, string NombreDistribuidor)>> 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<PorcPago, string, (PorcPago, string)>(
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<PorcPago?> 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<PorcPago>(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<PorcPago?> 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<PorcPago>(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<PorcPago?> 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<PorcPago>(sql,
new { IdPublicacionParam = idPublicacion, IdDistribuidorParam = idDistribuidor, VigenciaDNuevoParam = vigenciaDNuevo.Date },
transaction);
}
public async Task<PorcPago?> 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<PorcPago>(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<bool> UpdateAsync(PorcPago porcPagoAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PorcPago>(
@"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<bool> DeleteAsync(int idPorcentaje, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PorcPago>(
@"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<bool> 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<PorcPago>(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<PorcPago>(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;
}
}

View File

@@ -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<IEnumerable<Precio>> 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<Precio>(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<Precio>(sql, new { IdPublicacionParam = idPublicacion });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener precios por IdPublicacion: {IdPublicacion}", idPublicacion);
return Enumerable.Empty<Precio>();
}
}
public async Task<Precio?> GetByIdAsync(int idPrecio)
{
const string sql = "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio";
using var connection = _connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Precio>(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<Precio>(sql, new { IdPrecioParam = idPrecio });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener precio por ID: {idPrecio}", idPrecio);
return null;
}
}
public async Task<Precio?> 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<Precio>(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<Precio>(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<Precio?> 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<Precio>(sql, new { IdPublicacion = idPublicacion, VigenciaDNuevo = vigenciaDNuevo }, transaction);
return await transaction.Connection!.QuerySingleOrDefaultAsync<Precio>(sql,
new { IdPublicacionParam = idPublicacion, VigenciaDNuevoParam = vigenciaDNuevo.Date },
transaction);
}
public async Task<Precio?> 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<Precio>(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<Precio>(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<bool> UpdateAsync(Precio precioAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<Precio>(
"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<bool> DeleteAsync(int idPrecio, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<Precio>(
"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<bool> 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<Precio>(selectPrecios, new { IdPublicacion = idPublicacion }, transaction);
public async Task<bool> 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<Precio>(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)
{

View File

@@ -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<PubliSeccionRepository> _log;
public PubliSeccionRepository(DbConnectionFactory cf, ILogger<PubliSeccionRepository> log) { _cf = cf; _log = log; }
private readonly DbConnectionFactory _cf;
private readonly ILogger<PubliSeccionRepository> _log;
public PubliSeccionRepository(DbConnectionFactory connectionFactory, ILogger<PubliSeccionRepository> logger)
{
_cf = connectionFactory;
_log = logger;
}
public async Task<IEnumerable<PubliSeccion>> 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<PubliSeccion>(sqlBuilder.ToString(), new { IdPublicacionParam = idPublicacion });
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener Secciones por Publicacion ID: {IdPublicacion}", idPublicacion);
return Enumerable.Empty<PubliSeccion>();
}
}
public async Task<PubliSeccion?> 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<PubliSeccion>(sql, new { IdSeccionParam = idSeccion });
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener Sección por ID: {IdSeccion}", idSeccion);
return null;
}
}
public async Task<bool> 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<bool>(sqlBuilder.ToString(), parameters);
}
catch (Exception ex)
{
_log.LogError(ex, "Error en ExistsByNameInPublicacionAsync. Nombre: {Nombre}, PubID: {IdPublicacion}", nombre, idPublicacion);
return true;
}
}
public async Task<bool> 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<int?>(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<PubliSeccion?> 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<PubliSeccion>(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<bool> UpdateAsync(PubliSeccion seccionAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PubliSeccion>(
@"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<bool> DeleteAsync(int idSeccion, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<PubliSeccion>(
@"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<bool> 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<PubliSeccion>(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<PubliSeccion>(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;
}
}

View File

@@ -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<IEnumerable<(Publicacion Publicacion, string NombreEmpresa)>> 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<Publicacion, string, (Publicacion, string)>(
var result = await connection.QueryAsync<Publicacion, string, (Publicacion, string)>(
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<Publicacion, string, (Publicacion, string)>(
var result = await connection.QueryAsync<Publicacion, string, (Publicacion?, string?)>(
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<Publicacion?> GetByIdSimpleAsync(int id)
{
const string sql = "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id";
using var connection = _connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Publicacion>(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<Publicacion>(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<bool> 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<bool>(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<bool>(sqlBuilder.ToString(), parameters);
}
public async Task<bool> 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<int?>(query, new { Id = id }) == 1) return true;
foreach (var query in checkQueries)
{
if (await connection.ExecuteScalarAsync<int?>(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<Publicacion?> 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<Publicacion>(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<Publicacion>(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<Publicacion>(
"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<Publicacion>(
"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;
}
}

View File

@@ -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<RecargoZonaRepository> _logger; // _log
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<RecargoZonaRepository> _logger;
public RecargoZonaRepository(DbConnectionFactory connectionFactory, ILogger<RecargoZonaRepository> logger)
{
@@ -18,47 +20,245 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
_logger = logger;
}
public async Task<bool> DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction)
public async Task<IEnumerable<(RecargoZona Recargo, string NombreZona)>> 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<RecargoZona>(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<RecargoZona, string, (RecargoZona, string)>(
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<RecargoZona?> 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<RecargoZona>(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<RecargoZona?> 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<RecargoZona>(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<RecargoZona?> 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<RecargoZona>(sql,
new { IdPublicacionParam = idPublicacion, IdZonaParam = idZona, VigenciaDNuevoParam = vigenciaDNuevo.Date },
transaction);
}
public async Task<RecargoZona?> 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<RecargoZona>(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<bool> UpdateAsync(RecargoZona recargoAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<RecargoZona>(
@"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<bool> DeleteAsync(int idRecargo, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<RecargoZona>(
@"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<bool> 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<RecargoZona>(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).
}
}

View File

@@ -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<SalidaOtroDestinoRepository> _log;
public SalidaOtroDestinoRepository(DbConnectionFactory cf, ILogger<SalidaOtroDestinoRepository> log)
{
_cf = cf;
_log = log;
}
public async Task<IEnumerable<SalidaOtroDestino>> 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<SalidaOtroDestino>(sqlBuilder.ToString(), parameters);
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener Salidas a Otros Destinos.");
return Enumerable.Empty<SalidaOtroDestino>();
}
}
public async Task<SalidaOtroDestino?> 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<SalidaOtroDestino>(sql, new { IdParteParam = idParte });
}
catch (Exception ex)
{
_log.LogError(ex, "Error al obtener SalidaOtroDestino por ID: {IdParte}", idParte);
return null;
}
}
public async Task<SalidaOtroDestino?> 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<SalidaOtroDestino>(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<bool> UpdateAsync(SalidaOtroDestino salidaAActualizar, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<SalidaOtroDestino>(
@"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<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction)
{
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<SalidaOtroDestino>(
@"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;
}
}
}

View File

@@ -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<RegTirada?> GetByIdAsync(int idRegistro);
Task<IEnumerable<RegTirada>> GetByCriteriaAsync(DateTime? fecha, int? idPublicacion, int? idPlanta);
Task<RegTirada?> CreateAsync(RegTirada nuevaTirada, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idRegistro, int idUsuario, IDbTransaction transaction); // Si se borra el registro principal
Task<bool> DeleteByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario, IDbTransaction transaction);
Task<RegTirada?> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta, IDbTransaction? transaction = null);
}
public interface IRegPublicacionSeccionRepository // Para bob_RegPublicaciones
{
Task<IEnumerable<RegPublicacionSeccion>> GetByFechaPublicacionPlantaAsync(DateTime fecha, int idPublicacion, int idPlanta);
Task<RegPublicacionSeccion?> CreateAsync(RegPublicacionSeccion nuevaSeccionTirada, int idUsuario, IDbTransaction transaction);
Task<bool> 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
}
}

View File

@@ -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<IEnumerable<StockBobina>> GetAllAsync( // Para listados generales, DTO se arma en servicio
int? idTipoBobina,
string? nroBobinaFilter,
int? idPlanta,
int? idEstadoBobina,
string? remitoFilter,
DateTime? fechaDesde,
DateTime? fechaHasta);
Task<StockBobina?> GetByIdAsync(int idBobina);
Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina
Task<StockBobina?> CreateAsync(StockBobina nuevaBobina, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(StockBobina bobinaAActualizar, int idUsuario, IDbTransaction transaction, string tipoMod = "Actualizada"); // tipoMod para historial
Task<bool> DeleteAsync(int idBobina, int idUsuario, IDbTransaction transaction); // Solo si está en estado "Disponible"
}
}

View File

@@ -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<RegTiradaRepository> _log;
public RegTiradaRepository(DbConnectionFactory cf, ILogger<RegTiradaRepository> log)
{
_cf = cf;
_log = log;
}
public async Task<RegTirada?> 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<RegTirada>(sql, new { IdRegistroParam = idRegistro });
}
public async Task<RegTirada?> 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<RegTirada>(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<IEnumerable<RegTirada>> 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<RegTirada>(sqlBuilder.ToString(), parameters);
}
public async Task<RegTirada?> 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<RegTirada>(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<bool> 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<bool> 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<RegPublicacionSeccionRepository> _log;
public RegPublicacionSeccionRepository(DbConnectionFactory cf, ILogger<RegPublicacionSeccionRepository> log){ _cf = cf; _log = log; }
public async Task<IEnumerable<RegPublicacionSeccion>> 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<RegPublicacionSeccion>(sql, new { FechaParam = fecha.Date, IdPublicacionParam = idPublicacion, IdPlantaParam = idPlanta });
}
public async Task<RegPublicacionSeccion?> 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<RegPublicacionSeccion>(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<bool> 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)
}
}
}

View File

@@ -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<StockBobinaRepository> _logger;
public StockBobinaRepository(DbConnectionFactory connectionFactory, ILogger<StockBobinaRepository> logger)
{
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task<IEnumerable<StockBobina>> 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<StockBobina>(sqlBuilder.ToString(), parameters);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener Stock de Bobinas con filtros.");
return Enumerable.Empty<StockBobina>();
}
}
public async Task<StockBobina?> 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<StockBobina>(sql, new { IdBobinaParam = idBobina });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener StockBobina por ID: {IdBobina}", idBobina);
return null;
}
}
public async Task<StockBobina?> 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<StockBobina>(sql, new { NroBobinaParam = nroBobina });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener StockBobina por NroBobina: {NroBobina}", nroBobina);
return null;
}
}
public async Task<StockBobina?> 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<StockBobina>(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<bool> UpdateAsync(StockBobina bobinaAActualizar, int idUsuario, IDbTransaction transaction, string tipoMod = "Actualizada")
{
// Obtener estado actual para el historial
var actual = await transaction.Connection!.QuerySingleOrDefaultAsync<StockBobina>(
@"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<bool> 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<StockBobina>(
@"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;
}
}
}

View File

@@ -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)
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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"
}
}

View File

@@ -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.
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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.
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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í.
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}
}

View File

@@ -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<DetalleSeccionTiradaDto> Secciones { get; set; } = new List<DetalleSeccionTiradaDto>();
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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<DetalleSeccionEnListadoDto> SeccionesImpresas { get; set; } = new List<DetalleSeccionEnListadoDto>();
public int TotalPaginasSumadas { get; set; } // Suma de CantPag de las secciones impresas
}
}

View File

@@ -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
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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)
}
}

View File

@@ -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"
}
}

View File

@@ -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<IDistribuidorService, DistribuidorService>();
builder.Services.AddScoped<IPublicacionRepository, PublicacionRepository>();
builder.Services.AddScoped<IPublicacionService, PublicacionService>();
builder.Services.AddScoped<IRecargoZonaRepository, RecargoZonaRepository>();
builder.Services.AddScoped<IRecargoZonaService, RecargoZonaService>();
builder.Services.AddScoped<IPorcPagoRepository, PorcPagoRepository>();
builder.Services.AddScoped<IPorcPagoService, PorcPagoService>();
builder.Services.AddScoped<IPorcMonCanillaRepository, PorcMonCanillaRepository>();
builder.Services.AddScoped<IPorcMonCanillaService, PorcMonCanillaService>();
builder.Services.AddScoped<IPubliSeccionRepository, PubliSeccionRepository>();
builder.Services.AddScoped<IPubliSeccionService, PubliSeccionService>();
builder.Services.AddScoped<IPrecioRepository, PrecioRepository>();
builder.Services.AddScoped<IPrecioService, PrecioService>();
builder.Services.AddScoped<IStockBobinaRepository, StockBobinaRepository>();
builder.Services.AddScoped<IStockBobinaService, StockBobinaService>();
builder.Services.AddScoped<IRegTiradaRepository, RegTiradaRepository>();
builder.Services.AddScoped<IRegPublicacionSeccionRepository, RegPublicacionSeccionRepository>();
builder.Services.AddScoped<ITiradaService, TiradaService>();
builder.Services.AddScoped<ISalidaOtroDestinoRepository, SalidaOtroDestinoRepository>();
builder.Services.AddScoped<ISalidaOtroDestinoService, SalidaOtroDestinoService>();
builder.Services.AddScoped<IEntradaSalidaDistRepository, EntradaSalidaDistRepository>();
builder.Services.AddScoped<IEntradaSalidaDistService, EntradaSalidaDistService>();
// --- 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<string>() // 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();

View File

@@ -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<EntradaSalidaDistService> _logger;
public EntradaSalidaDistService(
IEntradaSalidaDistRepository esRepository,
IPublicacionRepository publicacionRepository,
IDistribuidorRepository distribuidorRepository,
IPrecioRepository precioRepository,
IRecargoZonaRepository recargoZonaRepository,
IPorcPagoRepository porcPagoRepository,
ISaldoRepository saldoRepository,
IEmpresaRepository empresaRepository,
DbConnectionFactory connectionFactory,
ILogger<EntradaSalidaDistService> logger)
{
_esRepository = esRepository;
_publicacionRepository = publicacionRepository;
_distribuidorRepository = distribuidorRepository;
_precioRepository = precioRepository;
_recargoZonaRepository = recargoZonaRepository;
_porcPagoRepository = porcPagoRepository;
_saldoRepository = saldoRepository;
_empresaRepository = empresaRepository;
_connectionFactory = connectionFactory;
_logger = logger;
}
private async Task<EntradaSalidaDistDto> 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<decimal> 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<IEnumerable<EntradaSalidaDistDto>> 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<EntradaSalidaDistDto>();
foreach (var mov in movimientos)
{
dtos.Add(await MapToDto(mov));
}
return dtos;
}
public async Task<EntradaSalidaDistDto?> 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}");
}
}
}
}

View File

@@ -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<IEnumerable<EntradaSalidaDistDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento);
Task<EntradaSalidaDistDto?> 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);
}
}

View File

@@ -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<IEnumerable<PorcMonCanillaDto>> ObtenerPorPublicacionIdAsync(int idPublicacion);
Task<PorcMonCanillaDto?> 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);
}
}

View File

@@ -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<IEnumerable<PorcPagoDto>> ObtenerPorPublicacionIdAsync(int idPublicacion);
Task<PorcPagoDto?> 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);
}
}

View File

@@ -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<IEnumerable<PubliSeccionDto>> ObtenerPorPublicacionIdAsync(int idPublicacion, bool? soloActivas = null);
Task<PubliSeccionDto?> 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);
}
}

View File

@@ -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<IEnumerable<RecargoZonaDto>> ObtenerPorPublicacionIdAsync(int idPublicacion);
Task<RecargoZonaDto?> 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);
}
}

View File

@@ -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<IEnumerable<SalidaOtroDestinoDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino);
Task<SalidaOtroDestinoDto?> 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);
}
}

View File

@@ -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<PorcMonCanillaService> _logger;
public PorcMonCanillaService(
IPorcMonCanillaRepository porcMonCanillaRepository,
IPublicacionRepository publicacionRepository,
ICanillaRepository canillaRepository,
DbConnectionFactory connectionFactory,
ILogger<PorcMonCanillaService> 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<PorcMonCanillaDto?> 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<IEnumerable<PorcMonCanillaDto>> 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<PorcMonCanillaDto?> 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}");
}
}
}
}

View File

@@ -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<PorcPagoService> _logger;
public PorcPagoService(
IPorcPagoRepository porcPagoRepository,
IPublicacionRepository publicacionRepository,
IDistribuidorRepository distribuidorRepository,
DbConnectionFactory connectionFactory,
ILogger<PorcPagoService> 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<PorcPagoDto> 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<IEnumerable<PorcPagoDto>> ObtenerPorPublicacionIdAsync(int idPublicacion)
{
var data = await _porcPagoRepository.GetByPublicacionIdAsync(idPublicacion);
return data.Select(MapToDto);
}
public async Task<PorcPagoDto?> 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}");
}
}
}
}

View File

@@ -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<PubliSeccionService> _logger;
public PubliSeccionService(
IPubliSeccionRepository publiSeccionRepository,
IPublicacionRepository publicacionRepository,
DbConnectionFactory connectionFactory,
ILogger<PubliSeccionService> 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<IEnumerable<PubliSeccionDto>> ObtenerPorPublicacionIdAsync(int idPublicacion, bool? soloActivas = null)
{
var secciones = await _publiSeccionRepository.GetByPublicacionIdAsync(idPublicacion, soloActivas);
return secciones.Select(MapToDto);
}
public async Task<PubliSeccionDto?> 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}");
}
}
}
}

View File

@@ -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<RecargoZonaService> _logger;
public RecargoZonaService(
IRecargoZonaRepository recargoZonaRepository,
IPublicacionRepository publicacionRepository,
IZonaRepository zonaRepository,
DbConnectionFactory connectionFactory,
ILogger<RecargoZonaService> 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<RecargoZonaDto> 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<IEnumerable<RecargoZonaDto>> 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<RecargoZonaDto?> 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}");
}
}
}
}

View File

@@ -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<SalidaOtroDestinoService> _logger;
public SalidaOtroDestinoService(
ISalidaOtroDestinoRepository salidaRepository,
IPublicacionRepository publicacionRepository,
IOtroDestinoRepository otroDestinoRepository,
DbConnectionFactory connectionFactory,
ILogger<SalidaOtroDestinoService> logger)
{
_salidaRepository = salidaRepository;
_publicacionRepository = publicacionRepository;
_otroDestinoRepository = otroDestinoRepository;
_connectionFactory = connectionFactory;
_logger = logger;
}
private async Task<SalidaOtroDestinoDto> 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<IEnumerable<SalidaOtroDestinoDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDestino)
{
var salidas = await _salidaRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idDestino);
var dtos = new List<SalidaOtroDestinoDto>();
foreach (var salida in salidas)
{
dtos.Add(await MapToDto(salida));
}
return dtos;
}
public async Task<SalidaOtroDestinoDto?> 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}");
}
}
}
}

View File

@@ -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<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta);
Task<StockBobinaDto?> 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);
}
}

View File

@@ -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<IEnumerable<TiradaDto>> 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<TiradaDto?> 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);
}
}

View File

@@ -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<StockBobinaService> _logger;
public StockBobinaService(
IStockBobinaRepository stockBobinaRepository,
ITipoBobinaRepository tipoBobinaRepository,
IPlantaRepository plantaRepository,
IEstadoBobinaRepository estadoBobinaRepository,
IPublicacionRepository publicacionRepository,
IPubliSeccionRepository publiSeccionRepository,
DbConnectionFactory connectionFactory,
ILogger<StockBobinaService> 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<StockBobinaDto> 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<IEnumerable<StockBobinaDto>> 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<StockBobinaDto>();
foreach (var bobina in bobinas)
{
dtos.Add(await MapToDto(bobina));
}
return dtos;
}
public async Task<StockBobinaDto?> 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}");
}
}
}
}

View File

@@ -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<TiradaService> _logger;
public TiradaService(
IRegTiradaRepository regTiradaRepository,
IRegPublicacionSeccionRepository regPublicacionSeccionRepository,
IPublicacionRepository publicacionRepository,
IPlantaRepository plantaRepository,
IPubliSeccionRepository publiSeccionRepository,
DbConnectionFactory connectionFactory,
ILogger<TiradaService> logger)
{
_regTiradaRepository = regTiradaRepository;
_regPublicacionSeccionRepository = regPublicacionSeccionRepository;
_publicacionRepository = publicacionRepository;
_plantaRepository = plantaRepository;
_publiSeccionRepository = publiSeccionRepository;
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task<IEnumerable<TiradaDto>> ObtenerTiradasAsync(DateTime? fecha, int? idPublicacion, int? idPlanta)
{
var tiradasPrincipales = await _regTiradaRepository.GetByCriteriaAsync(fecha, idPublicacion, idPlanta);
var resultado = new List<TiradaDto>();
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<DetalleSeccionEnListadoDto>();
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<DetalleSeccionEnListadoDto>();
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}");
}
}
}
}

View File

@@ -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")]

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","cInNtDBnkQU/n2nz4dl\u002BN2Fu06kTT6IORBeDdunxooU="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","kULolnJcJq9du0a0dBwZaPVupTEFX15sai6mOONU2qk="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -1 +1 @@
{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","cInNtDBnkQU/n2nz4dl\u002BN2Fu06kTT6IORBeDdunxooU="],"CachedAssets":{},"CachedCopyCandidates":{}}
{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","kULolnJcJq9du0a0dBwZaPVupTEFX15sai6mOONU2qk="],"CachedAssets":{},"CachedCopyCandidates":{}}

View File

@@ -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<void>;
initialData?: EntradaSalidaDistDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [tipoMovimiento, setTipoMovimiento] = useState<'Salida' | 'Entrada'>('Salida');
const [cantidad, setCantidad] = useState<string>('');
const [remito, setRemito] = useState<string>('');
const [observacion, setObservacion] = useState('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Movimiento Distribuidor' : 'Registrar Movimiento Distribuidor'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
<InputLabel id="publicacion-esd-select-label">Publicación</InputLabel>
<Select labelId="publicacion-esd-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
disabled={loading || loadingDropdowns || isEditing}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required>
<InputLabel id="distribuidor-esd-select-label">Distribuidor</InputLabel>
<Select labelId="distribuidor-esd-select-label" label="Distribuidor" value={idDistribuidor}
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
disabled={loading || loadingDropdowns || isEditing}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{distribuidores.map((d) => (<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>))}
</Select>
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
</FormControl>
<TextField label="Fecha Movimiento" type="date" value={fecha} required
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
<FormControl component="fieldset" margin="dense" error={!!localErrors.tipoMovimiento} required>
<Typography component="legend" variant="body2" sx={{mb:0.5}}>Tipo de Movimiento</Typography>
<RadioGroup row value={tipoMovimiento} onChange={(e) => {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} >
<FormControlLabel value="Salida" control={<Radio size="small"/>} label="Salida (a Distribuidor)" disabled={loading || isEditing}/>
<FormControlLabel value="Entrada" control={<Radio size="small"/>} label="Entrada (de Distribuidor)" disabled={loading || isEditing}/>
</RadioGroup>
{localErrors.tipoMovimiento && <Typography color="error" variant="caption">{localErrors.tipoMovimiento}</Typography>}
</FormControl>
<TextField label="Nro. Remito" type="number" value={remito} required={!isEditing}
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading || isEditing} inputProps={{min:1}}
/>
<TextField label="Cantidad" type="number" value={cantidad} required
onChange={(e) => {setCantidad(e.target.value); handleInputChange('cantidad');}}
margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''}
disabled={loading} inputProps={{min:1}}
/>
<TextField label="Observación (Opcional)" value={observacion}
onChange={(e) => setObservacion(e.target.value)}
margin="dense" fullWidth multiline rows={2} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Movimiento')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default EntradaSalidaDistFormModal;

View File

@@ -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<void>;
idPublicacion: number;
initialData?: PorcMonCanillaDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const PorcMonCanillaFormModal: React.FC<PorcMonCanillaFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idCanilla, setIdCanilla] = useState<number | string>('');
const [vigenciaD, setVigenciaD] = useState('');
const [vigenciaH, setVigenciaH] = useState('');
const [porcMon, setPorcMon] = useState<string>('');
const [esPorcentaje, setEsPorcentaje] = useState(true); // Default a porcentaje
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Porcentaje/Monto Canillita' : 'Agregar Nuevo Porcentaje/Monto Canillita'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla}>
<InputLabel id="canilla-pmc-select-label" required>Canillita</InputLabel>
<Select labelId="canilla-pmc-select-label" label="Canillita" value={idCanilla}
onChange={(e) => {setIdCanilla(e.target.value as number); handleInputChange('idCanilla');}}
disabled={loading || loadingCanillitas || isEditing}
>
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
</Select>
{localErrors.idCanilla && <Typography color="error" variant="caption">{localErrors.idCanilla}</Typography>}
</FormControl>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
{isEditing && (
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
)}
<TextField label="Valor" type="number" value={porcMon} required
onChange={(e) => {setPorcMon(e.target.value); handleInputChange('porcMon');}}
margin="dense" fullWidth error={!!localErrors.porcMon} helperText={localErrors.porcMon || ''}
disabled={loading}
InputProps={{ startAdornment: esPorcentaje ? undefined : <InputAdornment position="start">$</InputAdornment>,
endAdornment: esPorcentaje ? <InputAdornment position="end">%</InputAdornment> : undefined }}
inputProps={{ step: "0.01", lang:"es-AR" }}
/>
<FormControlLabel
control={<Checkbox checked={esPorcentaje} onChange={(e) => setEsPorcentaje(e.target.checked)} disabled={loading}/>}
label="Es Porcentaje (si no, es Monto Fijo)" sx={{mt:1}}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.canillitas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.canillitas}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingCanillitas}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default PorcMonCanillaFormModal;

View File

@@ -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<void>;
idPublicacion: number;
initialData?: PorcPagoDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const PorcPagoFormModal: React.FC<PorcPagoFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd"
const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd"
const [porcentaje, setPorcentaje] = useState<string>('');
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Porcentaje de Pago' : 'Agregar Nuevo Porcentaje de Pago'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor}>
<InputLabel id="distribuidor-porc-select-label" required>Distribuidor</InputLabel>
<Select labelId="distribuidor-porc-select-label" label="Distribuidor" value={idDistribuidor}
onChange={(e) => {setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor');}}
disabled={loading || loadingDistribuidores || isEditing} // Distribuidor no se edita
>
<MenuItem value="" disabled><em>Seleccione un distribuidor</em></MenuItem>
{distribuidores.map((d) => (<MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>))}
</Select>
{localErrors.idDistribuidor && <Typography color="error" variant="caption">{localErrors.idDistribuidor}</Typography>}
</FormControl>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
{isEditing && (
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
)}
<TextField label="Porcentaje" type="number" value={porcentaje} required
onChange={(e) => {setPorcentaje(e.target.value); handleInputChange('porcentaje');}}
margin="dense" fullWidth error={!!localErrors.porcentaje} helperText={localErrors.porcentaje || ''}
disabled={loading}
InputProps={{ endAdornment: <InputAdornment position="end">%</InputAdornment> }}
inputProps={{ step: "0.01", lang:"es-AR" }}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.distribuidores && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.distribuidores}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDistribuidores}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Porcentaje')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default PorcPagoFormModal;

View File

@@ -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<void>;
idPublicacion: number; // Siempre necesario para la creación
initialData?: PubliSeccionDto | null;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const PubliSeccionFormModal: React.FC<PubliSeccionFormModalProps> = ({
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<string | null>(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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Sección' : 'Agregar Nueva Sección'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField label="Nombre de la Sección" value={nombre} required
onChange={(e) => {setNombre(e.target.value); handleInputChange();}}
margin="dense" fullWidth error={!!localErrorNombre} helperText={localErrorNombre || ''}
disabled={loading} autoFocus
/>
<FormControlLabel
control={<Checkbox checked={estado} onChange={(e) => setEstado(e.target.checked)} disabled={loading}/>}
label="Activa" sx={{mt:1}}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Sección')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default PubliSeccionFormModal;

View File

@@ -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<void>;
idPublicacion: number;
initialData?: RecargoZonaDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const RecargoZonaFormModal: React.FC<RecargoZonaFormModalProps> = ({
open,
onClose,
onSubmit,
idPublicacion,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idZona, setIdZona] = useState<number | string>('');
const [vigenciaD, setVigenciaD] = useState(''); // "yyyy-MM-dd"
const [vigenciaH, setVigenciaH] = useState(''); // "yyyy-MM-dd"
const [valor, setValor] = useState<string>('');
const [zonas, setZonas] = useState<ZonaDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Recargo por Zona' : 'Agregar Nuevo Recargo por Zona'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idZona}>
<InputLabel id="zona-recargo-select-label" required>Zona</InputLabel>
<Select labelId="zona-recargo-select-label" label="Zona" value={idZona}
onChange={(e) => {setIdZona(e.target.value as number); handleInputChange('idZona');}}
disabled={loading || loadingZonas || isEditing} // Zona no se edita
>
<MenuItem value="" disabled><em>Seleccione una zona</em></MenuItem>
{zonas.map((z) => (<MenuItem key={z.idZona} value={z.idZona}>{z.nombre}</MenuItem>))}
</Select>
{localErrors.idZona && <Typography color="error" variant="caption">{localErrors.idZona}</Typography>}
</FormControl>
<TextField label="Vigencia Desde" type="date" value={vigenciaD} required={!isEditing}
onChange={(e) => {setVigenciaD(e.target.value); handleInputChange('vigenciaD');}}
margin="dense" fullWidth error={!!localErrors.vigenciaD} helperText={localErrors.vigenciaD || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
{isEditing && (
<TextField label="Vigencia Hasta (Opcional)" type="date" value={vigenciaH}
onChange={(e) => {setVigenciaH(e.target.value); handleInputChange('vigenciaH');}}
margin="dense" fullWidth error={!!localErrors.vigenciaH} helperText={localErrors.vigenciaH || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
)}
<TextField label="Valor Recargo" type="number" value={valor} required
onChange={(e) => {setValor(e.target.value); handleInputChange('valor');}}
margin="dense" fullWidth error={!!localErrors.valor} helperText={localErrors.valor || ''}
disabled={loading}
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
inputProps={{ step: "0.01", lang:"es-AR" }}
/>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.zonas && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.zonas}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingZonas}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Recargo')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default RecargoZonaFormModal;

View File

@@ -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<void>;
initialData?: SalidaOtroDestinoDto | null; // Para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const SalidaOtroDestinoFormModal: React.FC<SalidaOtroDestinoFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idDestino, setIdDestino] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [cantidad, setCantidad] = useState<string>('');
const [observacion, setObservacion] = useState('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
{isEditing ? 'Editar Salida a Otro Destino' : 'Registrar Salida a Otro Destino'}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
<InputLabel id="publicacion-sod-select-label">Publicación</InputLabel>
<Select labelId="publicacion-sod-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
disabled={loading || loadingDropdowns || isEditing} // No se edita publicación
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<FormControl fullWidth margin="dense" error={!!localErrors.idDestino} required>
<InputLabel id="destino-sod-select-label">Otro Destino</InputLabel>
<Select labelId="destino-sod-select-label" label="Otro Destino" value={idDestino}
onChange={(e) => {setIdDestino(e.target.value as number); handleInputChange('idDestino');}}
disabled={loading || loadingDropdowns || isEditing} // No se edita destino
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{otrosDestinos.map((d) => (<MenuItem key={d.idDestino} value={d.idDestino}>{d.nombre}</MenuItem>))}
</Select>
{localErrors.idDestino && <Typography color="error" variant="caption">{localErrors.idDestino}</Typography>}
</FormControl>
<TextField label="Fecha" type="date" value={fecha} required
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing}
/>
<TextField label="Cantidad" type="number" value={cantidad} required
onChange={(e) => {setCantidad(e.target.value); handleInputChange('cantidad');}}
margin="dense" fullWidth error={!!localErrors.cantidad} helperText={localErrors.cantidad || ''}
disabled={loading} inputProps={{min:1}}
/>
<TextField label="Observación (Opcional)" value={observacion}
onChange={(e) => setObservacion(e.target.value)}
margin="dense" fullWidth multiline rows={3} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Registrar Salida')}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default SalidaOtroDestinoFormModal;

View File

@@ -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<void>;
bobinaActual: StockBobinaDto | null; // La bobina cuyo estado se va a cambiar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
open,
onClose,
onSubmit,
bobinaActual,
errorMessage,
clearErrorMessage
}) => {
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idSeccion, setIdSeccion] = useState<number | string>('');
const [obs, setObs] = useState('');
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDto[]>([]);
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
</Typography>
<Typography variant="body2" gutterBottom>
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId}>
<InputLabel id="nuevo-estado-select-label" required>Nuevo Estado</InputLabel>
<Select labelId="nuevo-estado-select-label" label="Nuevo Estado" value={nuevoEstadoId}
onChange={(e) => {setNuevoEstadoId(e.target.value as number); handleInputChange('nuevoEstadoId');}}
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
>
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
</Select>
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
</FormControl>
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
<>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion}>
<InputLabel id="publicacion-estado-select-label" required>Publicación</InputLabel>
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
</FormControl>
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion}>
<InputLabel id="seccion-estado-select-label" required>Sección</InputLabel>
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
onChange={(e) => {setIdSeccion(e.target.value as number); handleInputChange('idSeccion');}}
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
>
<MenuItem value="" disabled><em>{idPublicacion ? 'Seleccione sección' : 'Seleccione publicación primero'}</em></MenuItem>
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
</Select>
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
{localErrors.secciones && <Alert severity="warning" sx={{mt:0.5}}>{localErrors.secciones}</Alert>}
</FormControl>
</>
)}
<TextField label="Fecha Cambio de Estado" type="date" value={fechaCambioEstado} required
onChange={(e) => {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}}
margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''}
disabled={loading} InputLabelProps={{ shrink: true }}
/>
<TextField label="Observaciones (Opcional)" value={obs}
onChange={(e) => setObs(e.target.value)}
margin="dense" fullWidth multiline rows={3} disabled={loading}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}>
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaCambioEstadoModal;

View File

@@ -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<void>;
initialData: StockBobinaDto | null; // Siempre habrá initialData para editar
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const StockBobinaEditFormModal: React.FC<StockBobinaEditFormModalProps> = ({
open,
onClose,
onSubmit,
initialData,
errorMessage,
clearErrorMessage
}) => {
const [idTipoBobina, setIdTipoBobina] = useState<number | string>('');
const [nroBobina, setNroBobina] = useState('');
const [peso, setPeso] = useState<string>('');
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>
Editar Datos de Bobina (ID: {initialData.idBobina})
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="edit-tipo-bobina-select-label" required>Tipo Bobina</InputLabel>
<Select labelId="edit-tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina}
onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem>
{tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))}
</Select>
{localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>}
</FormControl>
<TextField label="Nro. Bobina" value={nroBobina} required
onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}}
margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}} autoFocus
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Peso (Kg)" type="number" value={peso} required
onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}}
margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''}
disabled={loading} sx={{flex:1, minWidth: '150px'}}
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="edit-planta-select-label" required>Planta Destino</InputLabel>
<Select labelId="edit-planta-select-label" label="Planta Destino" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>}
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Nro. Remito" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
<TextField label="Fecha Remito" type="date" value={fechaRemito} required
onChange={(e) => {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'}}
/>
</Box>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : 'Guardar Cambios'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaEditFormModal;

View File

@@ -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<void>; // Solo para crear
errorMessage?: string | null;
clearErrorMessage: () => void;
// initialData no es necesario para un modal de solo creación
}
const StockBobinaIngresoFormModal: React.FC<StockBobinaIngresoFormModalProps> = ({
open,
onClose,
onSubmit,
errorMessage,
clearErrorMessage
}) => {
const [idTipoBobina, setIdTipoBobina] = useState<number | string>('');
const [nroBobina, setNroBobina] = useState('');
const [peso, setPeso] = useState<string>('');
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [remito, setRemito] = useState('');
const [fechaRemito, setFechaRemito] = useState(''); // yyyy-MM-dd
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>Ingresar Nueva Bobina a Stock</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idTipoBobina} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="tipo-bobina-select-label" required>Tipo Bobina</InputLabel>
<Select labelId="tipo-bobina-select-label" label="Tipo Bobina" value={idTipoBobina}
onChange={(e) => {setIdTipoBobina(e.target.value as number); handleInputChange('idTipoBobina');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione un tipo</em></MenuItem>
{tiposBobina.map((t) => (<MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>))}
</Select>
{localErrors.idTipoBobina && <Typography color="error" variant="caption">{localErrors.idTipoBobina}</Typography>}
</FormControl>
<TextField label="Nro. Bobina" value={nroBobina} required
onChange={(e) => {setNroBobina(e.target.value); handleInputChange('nroBobina');}}
margin="dense" fullWidth error={!!localErrors.nroBobina} helperText={localErrors.nroBobina || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Peso (Kg)" type="number" value={peso} required
onChange={(e) => {setPeso(e.target.value); handleInputChange('peso');}}
margin="dense" fullWidth error={!!localErrors.peso} helperText={localErrors.peso || ''}
disabled={loading} sx={{flex:1, minWidth: '150px'}}
/>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: '200px'}}>
<InputLabel id="planta-select-label" required>Planta Destino</InputLabel>
<Select labelId="planta-select-label" label="Planta Destino" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); handleInputChange('idPlanta');}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione una planta</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption">{localErrors.idPlanta}</Typography>}
</FormControl>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 1, flexWrap: 'wrap' }}>
<TextField label="Nro. Remito" value={remito} required
onChange={(e) => {setRemito(e.target.value); handleInputChange('remito');}}
margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''}
disabled={loading} sx={{flex:1, minWidth: '200px'}}
/>
<TextField label="Fecha Remito" type="date" value={fechaRemito} required
onChange={(e) => {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'}}
/>
</Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : 'Ingresar Bobina'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default StockBobinaIngresoFormModal;

View File

@@ -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<void>;
errorMessage?: string | null;
clearErrorMessage: () => void;
}
const TiradaFormModal: React.FC<TiradaFormModalProps> = ({
open,
onClose,
onSubmit,
errorMessage,
clearErrorMessage
}) => {
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [ejemplares, setEjemplares] = useState<string>('');
// CORREGIDO: Usar el nuevo tipo para el estado del formulario de secciones
const [seccionesDeTirada, setSeccionesDeTirada] = useState<DetalleSeccionFormState[]>([]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [seccionesPublicacion, setSeccionesPublicacion] = useState<PubliSeccionDto[]>([]);
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<HTMLFormElement>) => {
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 (
<Modal open={open} onClose={onClose}>
<Box sx={modalStyle}>
<Typography variant="h6" component="h2" gutterBottom>Registrar Nueva Tirada</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
{/* ... (campos de Publicacion, Fecha, Planta, Ejemplares sin cambios) ... */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} sx={{flex:1, minWidth: 200}}>
<InputLabel id="publicacion-tirada-select-label" required>Publicación</InputLabel>
<Select labelId="publicacion-tirada-select-label" label="Publicación" value={idPublicacion}
onChange={(e) => { setIdPublicacion(e.target.value as number); setLocalErrors(p => ({...p, idPublicacion: null})); }}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{publicaciones.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre} ({p.nombreEmpresa})</MenuItem>))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>}
</FormControl>
<TextField label="Fecha Tirada" type="date" value={fecha} required
onChange={(e) => {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}}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl fullWidth margin="dense" error={!!localErrors.idPlanta} sx={{flex:1, minWidth: 200}}>
<InputLabel id="planta-tirada-select-label" required>Planta</InputLabel>
<Select labelId="planta-tirada-select-label" label="Planta" value={idPlanta}
onChange={(e) => {setIdPlanta(e.target.value as number); setLocalErrors(p => ({...p, idPlanta: null}));}}
disabled={loading || loadingDropdowns}
>
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
{plantas.map((p) => (<MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>))}
</Select>
{localErrors.idPlanta && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPlanta}</Typography>}
</FormControl>
<TextField label="Total Ejemplares" type="number" value={ejemplares} required
onChange={(e) => {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}}
/>
</Box>
<Typography variant="subtitle1" sx={{mt: 2, mb:1}}>Detalle de Secciones Impresas:</Typography>
{localErrors.seccionesArray && <Alert severity="error" sx={{mb:1}}>{localErrors.seccionesArray}</Alert>}
<Paper variant="outlined" sx={{p:1, mb:2, maxHeight: '250px', overflowY: 'auto'}}> {/* Permitir scroll en tabla de secciones */}
<TableContainer>
<Table size="small" stickyHeader> {/* stickyHeader para que cabecera quede fija */}
<TableHead>
<TableRow>
<TableCell sx={{fontWeight:'bold', minWidth: 200}}>Sección</TableCell>
<TableCell sx={{fontWeight:'bold', width: '150px'}}>Cant. Páginas</TableCell>
<TableCell align="right" sx={{width: '50px'}}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{seccionesDeTirada.map((sec, index) => (
<TableRow key={sec.idTemporal || index}> {/* Usar idTemporal para key */}
<TableCell sx={{py:0.5}}>
<FormControl fullWidth size="small" error={!!localErrors[`seccion_${index}_id`]}>
<Select value={sec.idSeccion} // Ahora idSeccion es number | ''
onChange={(e) => handleSeccionChange(index, 'idSeccion', e.target.value as number | '')}
disabled={loading || loadingDropdowns || seccionesPublicacion.length === 0}
displayEmpty
>
<MenuItem value="" disabled><em>Seleccionar</em></MenuItem>
{seccionesPublicacion.map(s => <MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>)}
</Select>
{localErrors[`seccion_${index}_id`] && <Typography color="error" variant="caption">{localErrors[`seccion_${index}_id`]}</Typography>}
</FormControl>
</TableCell>
<TableCell sx={{py:0.5}}>
<TextField type="number" size="small" fullWidth value={sec.cantPag}
onChange={(e) => handleSeccionChange(index, 'cantPag', e.target.value)}
error={!!localErrors[`seccion_${index}_pag`]}
helperText={localErrors[`seccion_${index}_pag`] || ''}
disabled={loading}
inputProps={{min:1}}
/>
</TableCell>
<TableCell align="right" sx={{py:0.5}}>
<IconButton onClick={() => handleRemoveSeccion(index)} size="small" color="error" disabled={loading}>
<DeleteOutlineIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{seccionesDeTirada.length === 0 && (
<TableRow>
<TableCell colSpan={3} align="center">
<Typography variant="caption" color="textSecondary">
Agregue secciones a la tirada.
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddSeccion} sx={{mt:1}} size="small" disabled={loading || !idPublicacion}>
Agregar Sección
</Button>
</Paper>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingDropdowns}>
{loading ? <CircularProgress size={24} /> : 'Registrar Tirada'}
</Button>
</Box>
</Box>
</Box>
</Modal>
);
};
export default TiradaFormModal;

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
export interface CreatePorcMonCanillaDto {
idPublicacion: number;
idCanilla: number;
vigenciaD: string; // "yyyy-MM-dd"
porcMon: number;
esPorcentaje: boolean;
}

View File

@@ -0,0 +1,6 @@
export interface CreatePorcPagoDto {
idPublicacion: number;
idDistribuidor: number;
vigenciaD: string; // "yyyy-MM-dd"
porcentaje: number;
}

View File

@@ -0,0 +1,5 @@
export interface CreatePubliSeccionDto {
idPublicacion: number;
nombre: string;
estado?: boolean; // Default true en backend
}

View File

@@ -0,0 +1,6 @@
export interface CreateRecargoZonaDto {
idPublicacion: number;
idZona: number;
vigenciaD: string; // "yyyy-MM-dd"
valor: number;
}

View File

@@ -0,0 +1,7 @@
export interface CreateSalidaOtroDestinoDto {
idPublicacion: number;
idDestino: number;
fecha: string; // "yyyy-MM-dd"
cantidad: number;
observacion?: string | null;
}

View File

@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More