Compare commits
3 Commits
feat/cierr
...
Dev-Anomal
| Author | SHA1 | Date | |
|---|---|---|---|
| 1020555db6 | |||
| 615cf282a1 | |||
| d040099b9a |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -160,6 +160,4 @@ junit.xml
|
||||
|
||||
# ===================================================================
|
||||
# Fin del archivo .gitignore
|
||||
# ===================================================================
|
||||
Backend/SQL
|
||||
.atl
|
||||
# ===================================================================
|
||||
@@ -51,7 +51,6 @@ namespace GestionIntegral.Api.Controllers
|
||||
private readonly IPerfilService _perfilService;
|
||||
private readonly IPermisoService _permisoService;
|
||||
private readonly ICambioParadaService _cambioParadaService;
|
||||
private readonly ICierreCuentaCorrienteService _cierreCcService;
|
||||
private readonly ILogger<AuditoriaController> _logger;
|
||||
|
||||
// Permiso general para ver cualquier auditoría.
|
||||
@@ -87,7 +86,6 @@ namespace GestionIntegral.Api.Controllers
|
||||
IPerfilService perfilService,
|
||||
IPermisoService permisoService,
|
||||
ICambioParadaService cambioParadaService,
|
||||
ICierreCuentaCorrienteService cierreCcService,
|
||||
ILogger<AuditoriaController> logger)
|
||||
{
|
||||
_usuarioService = usuarioService;
|
||||
@@ -118,7 +116,6 @@ namespace GestionIntegral.Api.Controllers
|
||||
_perfilService = perfilService;
|
||||
_cambioParadaService = cambioParadaService;
|
||||
_permisoService = permisoService;
|
||||
_cierreCcService = cierreCcService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -695,26 +692,6 @@ namespace GestionIntegral.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("cierres-cuenta-corriente")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CierreCuentaCorrienteHistorialDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetHistorialCierresCuentaCorriente(
|
||||
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta,
|
||||
[FromQuery] int? idUsuarioModifico, [FromQuery] string? tipoModificacion,
|
||||
[FromQuery] int? idCierreAfectado)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerAuditoria)) return Forbid();
|
||||
try
|
||||
{
|
||||
var historial = await _cierreCcService.ObtenerHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idCierreAfectado);
|
||||
return Ok(historial ?? Enumerable.Empty<CierreCuentaCorrienteHistorialDto>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error obteniendo historial de Cierres de Cuenta Corriente.");
|
||||
return StatusCode(500, "Error interno al obtener historial de Cierres de Cuenta Corriente.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("cambios-parada-canilla")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CambioParadaHistorialDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetHistorialCambiosParada(
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
using GestionIntegral.Api.Dtos.Auditoria;
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using GestionIntegral.Api.Services.Contables;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Contables
|
||||
{
|
||||
[Route("api/cierres-cc")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class CierresCuentaCorrienteController : ControllerBase
|
||||
{
|
||||
private readonly ICierreCuentaCorrienteService _cierreService;
|
||||
private readonly ILogger<CierresCuentaCorrienteController> _logger;
|
||||
|
||||
// Permisos asignables a perfiles. La reapertura (CC002) NO se valida acá: es exclusiva de SuperAdmin.
|
||||
private const string PermisoCrear = "CC001";
|
||||
private const string PermisoVer = "CC003";
|
||||
|
||||
public CierresCuentaCorrienteController(
|
||||
ICierreCuentaCorrienteService cierreService,
|
||||
ILogger<CierresCuentaCorrienteController> logger)
|
||||
{
|
||||
_cierreService = cierreService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private bool TienePermiso(string codAcc) =>
|
||||
User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||
|
||||
private bool EsSuperAdmin() => User.IsInRole("SuperAdmin");
|
||||
|
||||
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 CierresCuentaCorrienteController.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// POST: api/cierres-cc
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CierreCuentaCorrienteDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Crear([FromBody] CrearCierreDto dto)
|
||||
{
|
||||
if (!TienePermiso(PermisoCrear)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (cierre, errorCode, errorMessage) = await _cierreService.CrearCierreAsync(dto, userId.Value);
|
||||
|
||||
if (errorCode != null)
|
||||
{
|
||||
int status = errorCode switch
|
||||
{
|
||||
"CIERRE_FECHA_ANTERIOR_A_ULTIMO" => StatusCodes.Status409Conflict,
|
||||
"CIERRE_ERROR_INTERNO" => StatusCodes.Status500InternalServerError,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
return StatusCode(status, new { codigo = errorCode, mensaje = errorMessage });
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status201Created, cierre);
|
||||
}
|
||||
|
||||
// POST: api/cierres-cc/{idCierre}/reabrir
|
||||
[HttpPost("{idCierre:int}/reabrir")]
|
||||
[ProducesResponseType(typeof(CierreCuentaCorrienteDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Reabrir(int idCierre, [FromBody] ReabrirCierreDto dto)
|
||||
{
|
||||
if (!EsSuperAdmin())
|
||||
return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (cierre, errorCode, errorMessage) = await _cierreService.ReabrirCierreAsync(idCierre, dto, userId.Value, esSuperAdmin: true);
|
||||
|
||||
if (errorCode != null)
|
||||
{
|
||||
int status = errorCode switch
|
||||
{
|
||||
"CIERRE_NO_ENCONTRADO" => StatusCodes.Status404NotFound,
|
||||
"CIERRE_PERMISO_DENEGADO" => StatusCodes.Status403Forbidden,
|
||||
"CIERRE_HAY_POSTERIORES_VIGENTES" => StatusCodes.Status409Conflict,
|
||||
"CIERRE_YA_ANULADO" => StatusCodes.Status409Conflict,
|
||||
"CIERRE_ERROR_INTERNO" => StatusCodes.Status500InternalServerError,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
return StatusCode(status, new { codigo = errorCode, mensaje = errorMessage });
|
||||
}
|
||||
|
||||
return Ok(cierre);
|
||||
}
|
||||
|
||||
// GET: api/cierres-cc?idDistribuidor=&idEmpresa=&estado=&fechaCorteDesde=&fechaCorteHasta=
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<CierreCuentaCorrienteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAll(
|
||||
[FromQuery] int? idDistribuidor,
|
||||
[FromQuery] int? idEmpresa,
|
||||
[FromQuery] string? estado,
|
||||
[FromQuery] DateTime? fechaCorteDesde,
|
||||
[FromQuery] DateTime? fechaCorteHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||
var cierres = await _cierreService.GetAllAsync(idDistribuidor, idEmpresa, estado, fechaCorteDesde, fechaCorteHasta);
|
||||
return Ok(cierres);
|
||||
}
|
||||
|
||||
// GET: api/cierres-cc/{idCierre}
|
||||
[HttpGet("{idCierre:int}")]
|
||||
[ProducesResponseType(typeof(CierreCuentaCorrienteDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetById(int idCierre)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||
var cierre = await _cierreService.GetByIdAsync(idCierre);
|
||||
if (cierre == null) return NotFound(new { message = $"Cierre #{idCierre} no encontrado." });
|
||||
return Ok(cierre);
|
||||
}
|
||||
|
||||
// GET: api/cierres-cc/ultimo?idDistribuidor=&idEmpresa=
|
||||
// Atajo del frontend para autorrellenar "Desde último cierre" en filtros del reporte.
|
||||
// Acepta CC003 (gestión de cierres) o RR001 (acceso al reporte que CONTIENE este atajo):
|
||||
// operadores con solo el permiso del reporte deben poder usar el atajo desde la pantalla del reporte.
|
||||
[HttpGet("ultimo")]
|
||||
[ProducesResponseType(typeof(UltimoCierreDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetUltimoVigente(
|
||||
[FromQuery] int idDistribuidor,
|
||||
[FromQuery] int idEmpresa)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer) && !TienePermiso("RR001")) return Forbid();
|
||||
var ultimo = await _cierreService.GetUltimoVigenteAsync(idDistribuidor, idEmpresa);
|
||||
if (ultimo == null) return NotFound(new { message = "No hay cierres vigentes para el distribuidor en la empresa indicada." });
|
||||
return Ok(ultimo);
|
||||
}
|
||||
|
||||
// GET: api/cierres-cc/{idCierre}/historial
|
||||
[HttpGet("{idCierre:int}/historial")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CierreCuentaCorrienteHistorialDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetHistorial(int idCierre)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||
var historial = await _cierreService.GetHistorialAsync(idCierre);
|
||||
return Ok(historial);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,9 @@ namespace GestionIntegral.Api.Controllers.Contables
|
||||
private readonly ISaldoService _saldoService;
|
||||
private readonly ILogger<SaldosController> _logger;
|
||||
|
||||
// Permiso para ver saldos. El ajuste manual es exclusivo de SuperAdmin (no se valida un permiso asignable).
|
||||
private const string PermisoVerSaldos = "CS001";
|
||||
// Define un permiso específico para ver saldos, y otro para ajustarlos (SuperAdmin implícito)
|
||||
private const string PermisoVerSaldos = "CS001"; // Ejemplo: Cuentas Saldos Ver
|
||||
private const string PermisoAjustarSaldos = "CS002"; // Ejemplo: Cuentas Saldos Ajustar (o solo SuperAdmin)
|
||||
|
||||
|
||||
public SaldosController(ISaldoService saldoService, ILogger<SaldosController> logger)
|
||||
@@ -75,11 +76,11 @@ namespace GestionIntegral.Api.Controllers.Contables
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> AjustarSaldoManualmente([FromBody] AjusteSaldoRequestDto ajusteDto)
|
||||
{
|
||||
// El ajuste manual de saldo es operación crítica: solo SuperAdmin. No se admite vía permiso asignable.
|
||||
if (!User.IsInRole("SuperAdmin"))
|
||||
// Esta operación debería ser MUY restringida. Solo SuperAdmin o un permiso muy específico.
|
||||
if (!User.IsInRole("SuperAdmin") && !TienePermiso(PermisoAjustarSaldos))
|
||||
{
|
||||
_logger.LogWarning("Intento no autorizado de ajustar saldo por Usuario ID {userId}", GetCurrentUserId() ?? 0);
|
||||
return Forbid("Solo SuperAdmin puede ajustar saldos manualmente.");
|
||||
return Forbid("No tiene permisos para realizar ajustes manuales de saldo.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
@@ -98,10 +99,6 @@ namespace GestionIntegral.Api.Controllers.Contables
|
||||
}
|
||||
return Ok(saldoActualizado);
|
||||
}
|
||||
catch (BloqueoPorPeriodoCerradoException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error crítico al ajustar saldo manualmente.");
|
||||
|
||||
@@ -40,19 +40,19 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc, [FromQuery] bool? soloActivos = true)
|
||||
public async Task<IActionResult> GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc, soloActivos);
|
||||
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc);
|
||||
return Ok(distribuidores);
|
||||
}
|
||||
|
||||
[HttpGet("dropdown")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAllDropdownDistribuidores([FromQuery] bool? soloActivos = true)
|
||||
public async Task<IActionResult> GetAllDropdownDistribuidores()
|
||||
{
|
||||
var distribuidores = await _distribuidorService.GetAllDropdownAsync(soloActivos);
|
||||
var distribuidores = await _distribuidorService.GetAllDropdownAsync();
|
||||
return Ok(distribuidores);
|
||||
}
|
||||
|
||||
@@ -117,27 +117,6 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/toggle-baja")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ToggleBajaDistribuidor(int id, [FromBody] ToggleBajaDistribuidorDto dto)
|
||||
{
|
||||
if (!TienePermiso(PermisoModificar)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _distribuidorService.ToggleBajaAsync(id, dto.DarDeBaja, dto.FechaBaja, userId.Value);
|
||||
if (!exito)
|
||||
{
|
||||
if (error == "Distribuidor no encontrado.") return NotFound(new { message = error });
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -41,7 +40,6 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET: api/stockbobinas
|
||||
// GET: api/stockbobinas
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||
@@ -49,23 +47,12 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
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,
|
||||
[FromQuery] DateTime? fechaEstadoDesde, [FromQuery] DateTime? fechaEstadoHasta) // <--- Nuevos parámetros agregados
|
||||
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerStock)) return Forbid();
|
||||
try
|
||||
{
|
||||
var bobinas = await _stockBobinaService.ObtenerTodosAsync(
|
||||
idTipoBobina,
|
||||
nroBobina,
|
||||
idPlanta,
|
||||
idEstadoBobina,
|
||||
remito,
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
fechaEstadoDesde,
|
||||
fechaEstadoHasta
|
||||
);
|
||||
var bobinas = await _stockBobinaService.ObtenerTodosAsync(idTipoBobina, nroBobina, idPlanta, idEstadoBobina, remito, fechaDesde, fechaHasta);
|
||||
return Ok(bobinas);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -140,7 +127,7 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState); // Validaciones de DTO (Required, Range, etc.)
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
|
||||
// La validación de que IdPublicacion/IdSeccion son requeridos para estado "En Uso"
|
||||
// ahora está más robusta en el servicio. Se puede quitar del controlador
|
||||
// si se prefiere que el servicio sea la única fuente de verdad para esa lógica.
|
||||
@@ -185,72 +172,5 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// GET: api/stockbobinas/verificar-remito
|
||||
[HttpGet("verificar-remito")]
|
||||
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
// CAMBIO: Hacer fechaRemito opcional (nullable)
|
||||
public async Task<IActionResult> VerificarRemito([FromQuery, BindRequired] int idPlanta, [FromQuery, BindRequired] string remito, [FromQuery] DateTime? fechaRemito)
|
||||
{
|
||||
if (!TienePermiso(PermisoIngresarBobina)) return Forbid();
|
||||
|
||||
try
|
||||
{
|
||||
// Pasamos el parámetro nullable al servicio
|
||||
var bobinasExistentes = await _stockBobinaService.VerificarRemitoExistenteAsync(idPlanta, remito, fechaRemito);
|
||||
return Ok(bobinasExistentes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al verificar remito {Remito} para planta {IdPlanta}", remito, idPlanta);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al verificar el remito.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("actualizar-fecha-remito")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> ActualizarFechaRemitoLote([FromBody] UpdateFechaRemitoLoteDto dto)
|
||||
{
|
||||
// Reutilizamos el permiso de modificar datos, ya que es una corrección.
|
||||
if (!TienePermiso(PermisoModificarDatos)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _stockBobinaService.ActualizarFechaRemitoLoteAsync(dto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// POST: api/stockbobinas/lote
|
||||
[HttpPost("lote")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> IngresarLoteBobinas([FromBody] CreateStockBobinaLoteDto loteDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoIngresarBobina)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _stockBobinaService.IngresarBobinaLoteAsync(loteDto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return NoContent(); // 204 es una buena respuesta para un lote procesado exitosamente sin devolver contenido.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
if (Model.DebitosCreditos.Any()) column.Item().Element(ComposeDebCredTable);
|
||||
|
||||
column.Item().Element(ComposeResumenPeriodo);
|
||||
column.Item().Element(ComposeSaldoFinal);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,11 +107,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Saldo");
|
||||
});
|
||||
|
||||
// Fila Saldo Inicial al inicio de la primera tabla — ancla del acumulado
|
||||
table.Cell().ColumnSpan(6).Border(1).Padding(2).Text(t => t.Span("Saldo Inicial").SemiBold());
|
||||
table.Cell().Border(1).Padding(2).AlignRight().Text(t => t.Span(Model.SaldoInicial.ToString("C", CultureAr)).SemiBold());
|
||||
|
||||
decimal saldoAcumulado = Model.SaldoInicial;
|
||||
decimal saldoAcumulado = 0; // Inicia en CERO
|
||||
foreach (var item in Model.Movimientos.OrderBy(m => m.Fecha))
|
||||
{
|
||||
saldoAcumulado += (item.Debe - item.Haber);
|
||||
@@ -160,7 +157,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).Text("Detalle");
|
||||
});
|
||||
|
||||
decimal saldoAcumulado = Model.SaldoInicial + Model.TotalMovimientos;
|
||||
decimal saldoAcumulado = Model.TotalMovimientos;
|
||||
foreach (var item in Model.Pagos.OrderBy(p => p.Fecha).ThenBy(p => p.Recibo))
|
||||
{
|
||||
saldoAcumulado += (item.Debe - item.Haber);
|
||||
@@ -207,7 +204,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
header.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(2).AlignRight().Text("Saldo");
|
||||
});
|
||||
|
||||
decimal saldoAcumulado = Model.SaldoInicial + Model.TotalMovimientos + Model.TotalPagos;
|
||||
decimal saldoAcumulado = Model.TotalMovimientos + Model.TotalPagos;
|
||||
foreach (var item in Model.DebitosCreditos.OrderBy(dc => dc.Fecha))
|
||||
{
|
||||
saldoAcumulado += (item.Debe - item.Haber);
|
||||
@@ -239,17 +236,25 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
row.RelativeItem().Text(label);
|
||||
row.ConstantItem(120).AlignRight().Text(value.ToString("C", CultureAr));
|
||||
};
|
||||
col.Item().Row(row => AddResumenRow(row, "Saldo Inicial", Model.SaldoInicial));
|
||||
col.Item().Row(row => AddResumenRow(row, "Movimientos", Model.TotalMovimientos));
|
||||
col.Item().Row(row => AddResumenRow(row, "Débitos/Créditos", Model.TotalDebitosCreditos));
|
||||
col.Item().Row(row => AddResumenRow(row, "Pagos", Model.TotalPagos));
|
||||
col.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(5).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text("Saldo Final").SemiBold();
|
||||
row.ConstantItem(120).AlignRight().Text(t => t.Span(Model.SaldoFinal.ToString("C", CultureAr)).SemiBold());
|
||||
row.RelativeItem().Text("Total").SemiBold();
|
||||
row.ConstantItem(120).AlignRight().Text(t => t.Span(Model.TotalPeriodo.ToString("C", CultureAr)).SemiBold());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ComposeSaldoFinal(IContainer container)
|
||||
{
|
||||
container.PaddingTop(5, Unit.Millimetre).AlignLeft().Text(text =>
|
||||
{
|
||||
text.Span($"Saldo Total del Distribuidor al {Model.FechaReporte} ").SemiBold().FontSize(12);
|
||||
text.Span(Model.SaldoDeCuenta.ToString("C", CultureAr)).SemiBold().FontSize(12);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
|
||||
foreach (var item in Model.PromediosPorDia.OrderBy(d => dayOrder.GetValueOrDefault(d.Dia, 99)))
|
||||
{
|
||||
var porcDevolucion = item.Promedio_Llevados > 0 ? (decimal)item.Promedio_Devueltos * 100 / item.Promedio_Llevados : 0;
|
||||
var porcDevolucion = item.Llevados > 0 ? (decimal)item.Devueltos * 100 / item.Llevados : 0;
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(item.Dia);
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant.ToString("N0"));
|
||||
@@ -162,6 +162,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
var general = Model.PromedioGeneral;
|
||||
if (general != null)
|
||||
{
|
||||
var porcDevolucionGeneral = general.Promedio_Llevados > 0 ? (decimal)general.Promedio_Devueltos * 100 / general.Promedio_Llevados : 0;
|
||||
var boldStyle = TextStyle.Default.SemiBold();
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(text => text.Span(general.Dia).Style(boldStyle));
|
||||
@@ -169,7 +170,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Llevados.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Devueltos.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Ventas.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(Model.PorcentajeDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
{
|
||||
page.Margin(1, Unit.Centimetre);
|
||||
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(10));
|
||||
|
||||
|
||||
page.Header().Element(ComposeHeader);
|
||||
page.Content().Element(ComposeContent);
|
||||
page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); });
|
||||
@@ -46,7 +46,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void ComposeContent(IContainer container)
|
||||
{
|
||||
container.PaddingTop(8, Unit.Millimetre).Column(column =>
|
||||
@@ -54,7 +54,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
column.Spacing(15);
|
||||
column.Item().Text("Distribución").SemiBold().FontSize(12);
|
||||
column.Item().Element(ComposeDetalleDiarioTable);
|
||||
|
||||
|
||||
column.Item().PaddingTop(5, Unit.Millimetre);
|
||||
|
||||
column.Item().Text("Promedios").SemiBold().FontSize(12);
|
||||
@@ -90,14 +90,14 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
var llevados = item.Llevados ?? 0;
|
||||
var devueltos = item.Devueltos ?? 0;
|
||||
var ventaNetaDia = llevados - devueltos;
|
||||
if (llevados > 0)
|
||||
if(llevados > 0)
|
||||
{
|
||||
ventaNetaAcumulada += ventaNetaDia;
|
||||
conteoDias++;
|
||||
}
|
||||
var promedio = conteoDias > 0 ? ventaNetaAcumulada / conteoDias : 0;
|
||||
var porcDevolucion = llevados > 0 ? (decimal)devueltos * 100 / llevados : 0;
|
||||
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(item.Dia.ToString());
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(llevados.ToString("N0"));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(devueltos.ToString("N0"));
|
||||
@@ -105,12 +105,12 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(promedio.ToString("N0"));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(porcDevolucion.ToString("F2") + "%");
|
||||
}
|
||||
|
||||
|
||||
var totalLlevados = Model.DetalleDiario.Sum(i => i.Llevados ?? 0);
|
||||
var totalDevueltos = Model.DetalleDiario.Sum(i => i.Devueltos ?? 0);
|
||||
var totalVentaNeta = totalLlevados - totalDevueltos;
|
||||
var totalPorcDevolucion = totalLlevados > 0 ? (decimal)totalDevueltos * 100 / totalLlevados : 0;
|
||||
|
||||
|
||||
var boldStyle = TextStyle.Default.SemiBold();
|
||||
table.Cell().Border(1).Padding(3);
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(totalLlevados.ToString("N0")).Style(boldStyle));
|
||||
@@ -120,11 +120,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(totalPorcDevolucion.ToString("F2") + "%").Style(boldStyle));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void ComposePromediosTable(IContainer container)
|
||||
{
|
||||
var dayOrder = new Dictionary<string, int> { { "Lunes", 1 }, { "Martes", 2 }, { "Miércoles", 3 }, { "Jueves", 4 }, { "Viernes", 5 }, { "Sábado", 6 }, { "Domingo", 7 } };
|
||||
|
||||
var dayOrder = new Dictionary<string, int> { { "Lunes", 1 }, { "Martes", 2 }, { "Miércoles", 3 }, { "Jueves", 4 }, { "Viernes", 5 }, { "Sábado", 6 }, { "Domingo", 7 }};
|
||||
|
||||
container.Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
@@ -147,7 +147,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
var llevados = item.Promedio_Llevados ?? 0;
|
||||
var devueltos = item.Promedio_Devueltos ?? 0;
|
||||
var porcDevolucion = llevados > 0 ? (decimal)devueltos * 100 / llevados : 0;
|
||||
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(item.Dia);
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant?.ToString("N0") ?? "0");
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(llevados.ToString("N0"));
|
||||
@@ -161,16 +161,16 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
if (general != null)
|
||||
{
|
||||
var boldStyle = TextStyle.Default.SemiBold();
|
||||
var avgPercentage = Model.PromediosPorDia
|
||||
.Where(p => p.Dia != "General" && (p.Promedio_Llevados ?? 0) > 0)
|
||||
.Average(p => (decimal)(p.Promedio_Devueltos ?? 0) * 100 / (p.Promedio_Llevados ?? 1));
|
||||
var llevadosGeneral = general.Llevados ?? 0; // Usamos el total para el %
|
||||
var devueltosGeneral = general.Devueltos ?? 0; // Usamos el total para el %
|
||||
var porcDevolucionGeneral = llevadosGeneral > 0 ? (decimal)devueltosGeneral * 100 / llevadosGeneral : 0;
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(t => t.Span(general.Dia).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Cant?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Llevados?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Devueltos?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Ventas?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(avgPercentage.ToString("F2") + "%").Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -894,11 +894,11 @@ namespace GestionIntegral.Api.Controllers
|
||||
{
|
||||
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
|
||||
|
||||
var (entradasSalidas, debitosCreditos, pagos, saldoInicial, error) =
|
||||
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
|
||||
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && saldoInicial == 0m)
|
||||
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && !saldos.Any())
|
||||
{
|
||||
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
|
||||
}
|
||||
@@ -911,7 +911,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
EntradasSalidas = entradasSalidas ?? Enumerable.Empty<BalanceCuentaDistDto>(),
|
||||
DebitosCreditos = debitosCreditos ?? Enumerable.Empty<BalanceCuentaDebCredDto>(),
|
||||
Pagos = pagos ?? Enumerable.Empty<BalanceCuentaPagosDto>(),
|
||||
SaldoInicial = saldoInicial,
|
||||
Saldos = saldos ?? Enumerable.Empty<SaldoDto>(),
|
||||
NombreDistribuidor = distribuidor.Distribuidor?.Nombre,
|
||||
NombreEmpresa = empresa?.Nombre
|
||||
};
|
||||
@@ -932,11 +932,11 @@ namespace GestionIntegral.Api.Controllers
|
||||
{
|
||||
if (!TienePermiso(PermisoVerBalanceCuentas)) return Forbid();
|
||||
|
||||
var (entradasSalidas, debitosCreditos, pagos, saldoInicial, error) =
|
||||
var (entradasSalidas, debitosCreditos, pagos, saldos, error) =
|
||||
await _reportesService.ObtenerReporteCuentasDistribuidorAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any() && saldoInicial == 0m)
|
||||
if (!entradasSalidas.Any() && !debitosCreditos.Any() && !pagos.Any())
|
||||
{
|
||||
return NotFound(new { message = "No hay datos para generar el reporte de cuenta del distribuidor." });
|
||||
}
|
||||
@@ -950,7 +950,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
Movimientos = entradasSalidas,
|
||||
Pagos = pagos,
|
||||
DebitosCreditos = debitosCreditos,
|
||||
SaldoInicial = saldoInicial,
|
||||
SaldoDeCuenta = saldos.FirstOrDefault()?.Monto ?? 0, // <-- Se asigna a SaldoDeCuenta
|
||||
NombreDistribuidor = distribuidor.Distribuidor?.Nombre ?? $"Distribuidor ID {idDistribuidor}",
|
||||
FechaDesde = fechaDesde.ToString("dd/MM/yyyy"),
|
||||
FechaHasta = fechaHasta.ToString("dd/MM/yyyy"),
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Dtos.Auditoria;
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using GestionIntegral.Api.Models.Contables;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
{
|
||||
public class CierreCuentaCorrienteRepository : ICierreCuentaCorrienteRepository
|
||||
{
|
||||
private readonly DbConnectionFactory _cf;
|
||||
private readonly ILogger<CierreCuentaCorrienteRepository> _log;
|
||||
|
||||
public CierreCuentaCorrienteRepository(DbConnectionFactory cf, ILogger<CierreCuentaCorrienteRepository> log)
|
||||
{
|
||||
_cf = cf;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
// Aliases SELECT — mapean Id_X (SQL) → IdX (modelo C#).
|
||||
private const string SelectModelBase = @"
|
||||
SELECT
|
||||
Id_Cierre AS IdCierre,
|
||||
Id_Distribuidor AS IdDistribuidor,
|
||||
Id_Empresa AS IdEmpresa,
|
||||
FechaCorte,
|
||||
FechaCierre,
|
||||
SaldoCierre,
|
||||
Estado,
|
||||
Justificacion,
|
||||
Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||
Id_Usuario_Anula AS IdUsuarioAnula,
|
||||
FechaAnulacion,
|
||||
Justificacion_Anulacion AS JustificacionAnulacion
|
||||
FROM dbo.cue_CierresCuentaCorriente";
|
||||
|
||||
public async Task<CierreCuentaCorriente?> GetByIdAsync(int idCierre)
|
||||
{
|
||||
var sql = SelectModelBase + " WHERE Id_Cierre = @IdCierreParam;";
|
||||
try
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, new { IdCierreParam = idCierre });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error al obtener Cierre por ID: {IdCierre}", idCierre);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CierreCuentaCorriente?> GetUltimoCierreVigenteAsync(int idDistribuidor, int idEmpresa, IDbTransaction? transaction = null)
|
||||
{
|
||||
var sql = @"
|
||||
SELECT TOP 1
|
||||
Id_Cierre AS IdCierre,
|
||||
Id_Distribuidor AS IdDistribuidor,
|
||||
Id_Empresa AS IdEmpresa,
|
||||
FechaCorte,
|
||||
FechaCierre,
|
||||
SaldoCierre,
|
||||
Estado,
|
||||
Justificacion,
|
||||
Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||
Id_Usuario_Anula AS IdUsuarioAnula,
|
||||
FechaAnulacion,
|
||||
Justificacion_Anulacion AS JustificacionAnulacion
|
||||
FROM dbo.cue_CierresCuentaCorriente
|
||||
WHERE Id_Distribuidor = @IdDist
|
||||
AND Id_Empresa = @IdEmp
|
||||
AND Estado = 'Activo'
|
||||
ORDER BY FechaCorte DESC, Id_Cierre DESC;";
|
||||
|
||||
var parameters = new { IdDist = idDistribuidor, IdEmp = idEmpresa };
|
||||
|
||||
try
|
||||
{
|
||||
if (transaction != null)
|
||||
{
|
||||
return await transaction.Connection!.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, parameters, transaction);
|
||||
}
|
||||
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error al obtener último cierre vigente Dist={IdDist} Emp={IdEmp}", idDistribuidor, idEmpresa);
|
||||
if (transaction != null) throw;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CierreCuentaCorriente?> GetCierreVigenteParaFechaAsync(int idDistribuidor, int idEmpresa, DateTime fechaOperacion)
|
||||
{
|
||||
// Cae en período cerrado si existe un cierre Activo cuya FechaCorte sea >= fechaOperacion (la fecha está dentro del período cerrado).
|
||||
// Se devuelve el más reciente (TOP 1 ORDER BY FechaCorte DESC) — el más restrictivo desde la perspectiva de la fecha consultada.
|
||||
const string sql = @"
|
||||
SELECT TOP 1
|
||||
Id_Cierre AS IdCierre,
|
||||
Id_Distribuidor AS IdDistribuidor,
|
||||
Id_Empresa AS IdEmpresa,
|
||||
FechaCorte,
|
||||
FechaCierre,
|
||||
SaldoCierre,
|
||||
Estado,
|
||||
Justificacion,
|
||||
Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||
Id_Usuario_Anula AS IdUsuarioAnula,
|
||||
FechaAnulacion,
|
||||
Justificacion_Anulacion AS JustificacionAnulacion
|
||||
FROM dbo.cue_CierresCuentaCorriente
|
||||
WHERE Id_Distribuidor = @IdDist
|
||||
AND Id_Empresa = @IdEmp
|
||||
AND Estado = 'Activo'
|
||||
AND FechaCorte >= @FechaOp
|
||||
ORDER BY FechaCorte DESC, Id_Cierre DESC;";
|
||||
try
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<CierreCuentaCorriente>(sql, new
|
||||
{
|
||||
IdDist = idDistribuidor,
|
||||
IdEmp = idEmpresa,
|
||||
FechaOp = fechaOperacion.Date
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error en GetCierreVigenteParaFechaAsync Dist={IdDist} Emp={IdEmp} Fecha={Fecha}", idDistribuidor, idEmpresa, fechaOperacion);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExisteCierrePosteriorVigenteAsync(int idDistribuidor, int idEmpresa, DateTime fechaCorte, int? excluirIdCierre = null, IDbTransaction? transaction = null)
|
||||
{
|
||||
const string sql = @"
|
||||
SELECT CASE WHEN EXISTS (
|
||||
SELECT 1 FROM dbo.cue_CierresCuentaCorriente
|
||||
WHERE Id_Distribuidor = @IdDist
|
||||
AND Id_Empresa = @IdEmp
|
||||
AND Estado = 'Activo'
|
||||
AND FechaCorte > @FechaCorte
|
||||
AND (@Excluir IS NULL OR Id_Cierre <> @Excluir)
|
||||
) THEN 1 ELSE 0 END;";
|
||||
|
||||
var parameters = new
|
||||
{
|
||||
IdDist = idDistribuidor,
|
||||
IdEmp = idEmpresa,
|
||||
FechaCorte = fechaCorte.Date,
|
||||
Excluir = excluirIdCierre
|
||||
};
|
||||
|
||||
if (transaction != null)
|
||||
{
|
||||
return await transaction.Connection!.ExecuteScalarAsync<bool>(sql, parameters, transaction);
|
||||
}
|
||||
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.ExecuteScalarAsync<bool>(sql, parameters);
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(CierreCuentaCorriente cierre, int idUsuarioMod, IDbTransaction transaction)
|
||||
{
|
||||
const string sqlInsertMaster = @"
|
||||
INSERT INTO dbo.cue_CierresCuentaCorriente
|
||||
(Id_Distribuidor, Id_Empresa, FechaCorte, FechaCierre, SaldoCierre, Estado, Justificacion, Id_Usuario_Cierre)
|
||||
VALUES
|
||||
(@IdDistribuidor, @IdEmpresa, @FechaCorte, @FechaCierre, @SaldoCierre, @Estado, @Justificacion, @IdUsuarioCierre);
|
||||
SELECT CAST(SCOPE_IDENTITY() AS INT);";
|
||||
|
||||
var idCierre = await transaction.Connection!.ExecuteScalarAsync<int>(sqlInsertMaster, new
|
||||
{
|
||||
cierre.IdDistribuidor,
|
||||
cierre.IdEmpresa,
|
||||
FechaCorte = cierre.FechaCorte.Date,
|
||||
cierre.FechaCierre,
|
||||
cierre.SaldoCierre,
|
||||
cierre.Estado,
|
||||
cierre.Justificacion,
|
||||
cierre.IdUsuarioCierre
|
||||
}, transaction);
|
||||
|
||||
if (idCierre <= 0) throw new DataException("No se pudo crear el cierre — SCOPE_IDENTITY no devolvió valor.");
|
||||
|
||||
const string sqlInsertHist = @"
|
||||
INSERT INTO dbo.cue_CierresCuentaCorriente_H
|
||||
(Id_Cierre, Id_Distribuidor, Id_Empresa, FechaCorte, FechaCierre, SaldoCierre,
|
||||
Estado, Justificacion, Id_Usuario_Cierre,
|
||||
Id_Usuario_Anula, FechaAnulacion, Justificacion_Anulacion,
|
||||
TipoMod, Id_Usuario_Mod, FechaMod)
|
||||
VALUES
|
||||
(@IdCierre, @IdDistribuidor, @IdEmpresa, @FechaCorte, @FechaCierre, @SaldoCierre,
|
||||
@Estado, @Justificacion, @IdUsuarioCierre,
|
||||
NULL, NULL, NULL,
|
||||
@TipoMod, @IdUsuarioMod, @FechaMod);";
|
||||
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHist, new
|
||||
{
|
||||
IdCierre = idCierre,
|
||||
cierre.IdDistribuidor,
|
||||
cierre.IdEmpresa,
|
||||
FechaCorte = cierre.FechaCorte.Date,
|
||||
cierre.FechaCierre,
|
||||
cierre.SaldoCierre,
|
||||
cierre.Estado,
|
||||
cierre.Justificacion,
|
||||
cierre.IdUsuarioCierre,
|
||||
TipoMod = "Creacion",
|
||||
IdUsuarioMod = idUsuarioMod,
|
||||
FechaMod = DateTime.Now
|
||||
}, transaction);
|
||||
|
||||
return idCierre;
|
||||
}
|
||||
|
||||
public async Task<bool> AnularAsync(int idCierre, int idUsuarioAnula, string justificacionAnulacion, int idUsuarioMod, IDbTransaction transaction)
|
||||
{
|
||||
// UPDATE atómico: solo cambia Estado si está actualmente Activo. Evita doble anulación concurrente.
|
||||
const string sqlUpdate = @"
|
||||
UPDATE dbo.cue_CierresCuentaCorriente
|
||||
SET Estado = 'Anulado',
|
||||
Id_Usuario_Anula = @IdUsuarioAnula,
|
||||
FechaAnulacion = @FechaAnulacion,
|
||||
Justificacion_Anulacion = @JustificacionAnulacion
|
||||
WHERE Id_Cierre = @IdCierre
|
||||
AND Estado = 'Activo';";
|
||||
|
||||
var fechaAnulacion = DateTime.Now;
|
||||
|
||||
int affected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new
|
||||
{
|
||||
IdCierre = idCierre,
|
||||
IdUsuarioAnula = idUsuarioAnula,
|
||||
FechaAnulacion = fechaAnulacion,
|
||||
JustificacionAnulacion = justificacionAnulacion
|
||||
}, transaction);
|
||||
|
||||
if (affected != 1) return false;
|
||||
|
||||
// Snapshot post-update para el _H. Trae los valores ya actualizados.
|
||||
const string sqlSnapshot = SelectModelBase + " WHERE Id_Cierre = @IdCierre;";
|
||||
var actualizado = await transaction.Connection!.QuerySingleAsync<CierreCuentaCorriente>(sqlSnapshot, new { IdCierre = idCierre }, transaction);
|
||||
|
||||
const string sqlInsertHist = @"
|
||||
INSERT INTO dbo.cue_CierresCuentaCorriente_H
|
||||
(Id_Cierre, Id_Distribuidor, Id_Empresa, FechaCorte, FechaCierre, SaldoCierre,
|
||||
Estado, Justificacion, Id_Usuario_Cierre,
|
||||
Id_Usuario_Anula, FechaAnulacion, Justificacion_Anulacion,
|
||||
TipoMod, Id_Usuario_Mod, FechaMod)
|
||||
VALUES
|
||||
(@IdCierre, @IdDistribuidor, @IdEmpresa, @FechaCorte, @FechaCierre, @SaldoCierre,
|
||||
@Estado, @Justificacion, @IdUsuarioCierre,
|
||||
@IdUsuarioAnula, @FechaAnulacion, @JustificacionAnulacion,
|
||||
@TipoMod, @IdUsuarioMod, @FechaMod);";
|
||||
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHist, new
|
||||
{
|
||||
IdCierre = actualizado.IdCierre,
|
||||
actualizado.IdDistribuidor,
|
||||
actualizado.IdEmpresa,
|
||||
FechaCorte = actualizado.FechaCorte.Date,
|
||||
actualizado.FechaCierre,
|
||||
actualizado.SaldoCierre,
|
||||
actualizado.Estado,
|
||||
actualizado.Justificacion,
|
||||
actualizado.IdUsuarioCierre,
|
||||
actualizado.IdUsuarioAnula,
|
||||
actualizado.FechaAnulacion,
|
||||
JustificacionAnulacion = actualizado.JustificacionAnulacion,
|
||||
TipoMod = "Reapertura",
|
||||
IdUsuarioMod = idUsuarioMod,
|
||||
FechaMod = DateTime.Now
|
||||
}, transaction);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
c.Id_Cierre AS IdCierre,
|
||||
c.Id_Distribuidor AS IdDistribuidor,
|
||||
d.Nombre AS NombreDistribuidor,
|
||||
c.Id_Empresa AS IdEmpresa,
|
||||
e.Nombre AS NombreEmpresa,
|
||||
CONVERT(varchar(10), c.FechaCorte, 23) AS FechaCorte,
|
||||
c.FechaCierre,
|
||||
c.SaldoCierre,
|
||||
c.Estado,
|
||||
c.Justificacion,
|
||||
c.Id_Usuario_Cierre AS IdUsuarioCierre,
|
||||
(uc.Nombre + ' ' + uc.Apellido) AS NombreUsuarioCierre,
|
||||
c.Id_Usuario_Anula AS IdUsuarioAnula,
|
||||
CASE WHEN ua.Id IS NULL THEN NULL ELSE (ua.Nombre + ' ' + ua.Apellido) END AS NombreUsuarioAnula,
|
||||
c.FechaAnulacion,
|
||||
c.Justificacion_Anulacion AS JustificacionAnulacion,
|
||||
CAST(CASE WHEN c.Estado = 'Activo'
|
||||
AND c.Id_Cierre = (
|
||||
SELECT TOP 1 c2.Id_Cierre FROM dbo.cue_CierresCuentaCorriente c2
|
||||
WHERE c2.Id_Distribuidor = c.Id_Distribuidor
|
||||
AND c2.Id_Empresa = c.Id_Empresa
|
||||
AND c2.Estado = 'Activo'
|
||||
ORDER BY c2.FechaCorte DESC, c2.Id_Cierre DESC)
|
||||
THEN 1 ELSE 0 END AS bit) AS EsUltimoVigente
|
||||
FROM dbo.cue_CierresCuentaCorriente c
|
||||
JOIN dbo.dist_dtDistribuidores d ON d.Id_Distribuidor = c.Id_Distribuidor
|
||||
JOIN dbo.dist_dtEmpresas e ON e.Id_Empresa = c.Id_Empresa
|
||||
JOIN dbo.gral_Usuarios uc ON uc.Id = c.Id_Usuario_Cierre
|
||||
LEFT JOIN dbo.gral_Usuarios ua ON ua.Id = c.Id_Usuario_Anula
|
||||
WHERE 1=1");
|
||||
|
||||
var parameters = new DynamicParameters();
|
||||
if (idDistribuidor.HasValue) { sqlBuilder.Append(" AND c.Id_Distribuidor = @IdDist"); parameters.Add("IdDist", idDistribuidor.Value); }
|
||||
if (idEmpresa.HasValue) { sqlBuilder.Append(" AND c.Id_Empresa = @IdEmp"); parameters.Add("IdEmp", idEmpresa.Value); }
|
||||
if (!string.IsNullOrWhiteSpace(estado)) { sqlBuilder.Append(" AND c.Estado = @Estado"); parameters.Add("Estado", estado); }
|
||||
if (fechaCorteDesde.HasValue) { sqlBuilder.Append(" AND c.FechaCorte >= @FechaDesde"); parameters.Add("FechaDesde", fechaCorteDesde.Value.Date); }
|
||||
if (fechaCorteHasta.HasValue) { sqlBuilder.Append(" AND c.FechaCorte <= @FechaHasta"); parameters.Add("FechaHasta", fechaCorteHasta.Value.Date); }
|
||||
sqlBuilder.Append(" ORDER BY c.FechaCorte DESC, c.Id_Cierre DESC;");
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.QueryAsync<CierreCuentaCorrienteDto>(sqlBuilder.ToString(), parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error en GetAllAsync de CierresCuentaCorriente.");
|
||||
return Enumerable.Empty<CierreCuentaCorrienteDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre)
|
||||
{
|
||||
const string sql = @"
|
||||
SELECT
|
||||
h.Id_Historial,
|
||||
h.Id_Cierre,
|
||||
h.Id_Distribuidor,
|
||||
h.Id_Empresa,
|
||||
h.FechaCorte,
|
||||
h.FechaCierre,
|
||||
h.SaldoCierre,
|
||||
h.Estado,
|
||||
h.Justificacion,
|
||||
h.Id_Usuario_Cierre,
|
||||
h.Id_Usuario_Anula,
|
||||
h.FechaAnulacion,
|
||||
h.Justificacion_Anulacion,
|
||||
h.TipoMod,
|
||||
h.Id_Usuario_Mod,
|
||||
(u.Nombre + ' ' + u.Apellido) AS NombreUsuarioModifico,
|
||||
h.FechaMod
|
||||
FROM dbo.cue_CierresCuentaCorriente_H h
|
||||
JOIN dbo.gral_Usuarios u ON u.Id = h.Id_Usuario_Mod
|
||||
WHERE h.Id_Cierre = @IdCierre
|
||||
ORDER BY h.FechaMod ASC, h.Id_Historial ASC;";
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.QueryAsync<CierreCuentaCorrienteHistorialDto>(sql, new { IdCierre = idCierre });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error en GetHistorialAsync para Cierre ID {IdCierre}", idCierre);
|
||||
return Enumerable.Empty<CierreCuentaCorrienteHistorialDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idCierreAfectado)
|
||||
{
|
||||
// FechaMod cubre el rango inclusive: [fechaDesde 00:00, fechaHasta+1día). Patrón consistente con otros ObtenerHistorial del proyecto.
|
||||
var sql = @"
|
||||
SELECT
|
||||
h.Id_Historial,
|
||||
h.Id_Cierre,
|
||||
h.Id_Distribuidor,
|
||||
h.Id_Empresa,
|
||||
h.FechaCorte,
|
||||
h.FechaCierre,
|
||||
h.SaldoCierre,
|
||||
h.Estado,
|
||||
h.Justificacion,
|
||||
h.Id_Usuario_Cierre,
|
||||
h.Id_Usuario_Anula,
|
||||
h.FechaAnulacion,
|
||||
h.Justificacion_Anulacion,
|
||||
h.TipoMod,
|
||||
h.Id_Usuario_Mod,
|
||||
(u.Nombre + ' ' + u.Apellido) AS NombreUsuarioModifico,
|
||||
h.FechaMod
|
||||
FROM dbo.cue_CierresCuentaCorriente_H h
|
||||
JOIN dbo.gral_Usuarios u ON u.Id = h.Id_Usuario_Mod
|
||||
WHERE 1 = 1
|
||||
AND (@FechaDesde IS NULL OR h.FechaMod >= @FechaDesde)
|
||||
AND (@FechaHasta IS NULL OR h.FechaMod < DATEADD(DAY, 1, @FechaHasta))
|
||||
AND (@IdUsuarioMod IS NULL OR h.Id_Usuario_Mod = @IdUsuarioMod)
|
||||
AND (@TipoMod IS NULL OR h.TipoMod = @TipoMod)
|
||||
AND (@IdCierre IS NULL OR h.Id_Cierre = @IdCierre)
|
||||
ORDER BY h.FechaMod DESC, h.Id_Historial DESC;";
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.QueryAsync<CierreCuentaCorrienteHistorialDto>(sql, new
|
||||
{
|
||||
FechaDesde = fechaDesde?.Date,
|
||||
FechaHasta = fechaHasta?.Date,
|
||||
IdUsuarioMod = idUsuarioModifico,
|
||||
TipoMod = tipoModificacion,
|
||||
IdCierre = idCierreAfectado
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error en ObtenerHistorialAsync (auditoría) de Cierres CC.");
|
||||
return Enumerable.Empty<CierreCuentaCorrienteHistorialDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using GestionIntegral.Api.Dtos.Auditoria;
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using GestionIntegral.Api.Models.Contables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
{
|
||||
public interface ICierreCuentaCorrienteRepository
|
||||
{
|
||||
Task<CierreCuentaCorriente?> GetByIdAsync(int idCierre);
|
||||
|
||||
// Devuelve el último cierre con Estado = 'Activo' para el par (Distribuidor + Empresa).
|
||||
// Acepta una transacción opcional para usarse dentro del flujo Crear/Reabrir.
|
||||
Task<CierreCuentaCorriente?> GetUltimoCierreVigenteAsync(int idDistribuidor, int idEmpresa, IDbTransaction? transaction = null);
|
||||
|
||||
// Verifica si la fecha cae dentro de un período cerrado: existe un cierre Activo con FechaCorte >= fechaOperacion.
|
||||
Task<CierreCuentaCorriente?> GetCierreVigenteParaFechaAsync(int idDistribuidor, int idEmpresa, DateTime fechaOperacion);
|
||||
|
||||
// True si existe otro cierre Activo con FechaCorte > fechaCorte (excluyendo opcionalmente un cierre puntual).
|
||||
// Se usa al reabrir para forzar la cascada manual.
|
||||
Task<bool> ExisteCierrePosteriorVigenteAsync(int idDistribuidor, int idEmpresa, DateTime fechaCorte, int? excluirIdCierre = null, IDbTransaction? transaction = null);
|
||||
|
||||
// Crea la fila maestra y registra la entrada inicial en _H con TipoMod='Creacion'. Devuelve el Id_Cierre generado.
|
||||
Task<int> CreateAsync(CierreCuentaCorriente cierre, int idUsuarioMod, IDbTransaction transaction);
|
||||
|
||||
// Marca un cierre como Anulado (UPDATE atómico solo si Estado='Activo') y registra entrada en _H con TipoMod='Reapertura'.
|
||||
// Devuelve true si se anuló (Estado pasó de Activo a Anulado), false si ya estaba anulado o no existía.
|
||||
Task<bool> AnularAsync(int idCierre, int idUsuarioAnula, string justificacionAnulacion, int idUsuarioMod, IDbTransaction transaction);
|
||||
|
||||
Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta);
|
||||
|
||||
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre);
|
||||
|
||||
// Auditoría general: filtra el historial cruzado de todos los cierres por rango de FechaMod, usuario, tipo, y opcional Id_Cierre.
|
||||
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idCierreAfectado);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
|
||||
public async Task<IEnumerable<int>> GetAllDistribuidorIdsAsync()
|
||||
{
|
||||
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores WHERE Baja = 0";
|
||||
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores";
|
||||
try
|
||||
{
|
||||
using (var connection = _connectionFactory.CreateConnection())
|
||||
@@ -138,45 +138,25 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
|
||||
public async Task<IEnumerable<Saldo>> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion
|
||||
FROM dbo.cue_Saldos s
|
||||
WHERE 1=1");
|
||||
|
||||
var sqlBuilder = new StringBuilder("SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion FROM dbo.cue_Saldos WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(destinoFilter))
|
||||
{
|
||||
sqlBuilder.Append(" AND s.Destino = @Destino");
|
||||
sqlBuilder.Append(" AND Destino = @Destino");
|
||||
parameters.Add("Destino", destinoFilter);
|
||||
|
||||
// Filtro para excluir distribuidores de baja si el tipo es Distribuidores
|
||||
// No se aplica a Canillas por requerimiento explícito del usuario
|
||||
if (destinoFilter == "Distribuidores")
|
||||
{
|
||||
sqlBuilder.Append(" AND EXISTS (SELECT 1 FROM dbo.dist_dtDistribuidores d WHERE d.Id_Distribuidor = s.Id_Destino AND d.Baja = 0)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Si no hay filtro de destino, aplicamos el filtro de baja solo para Distribuidores
|
||||
sqlBuilder.Append(@" AND (
|
||||
(s.Destino = 'Distribuidores' AND EXISTS (SELECT 1 FROM dbo.dist_dtDistribuidores d WHERE d.Id_Distribuidor = s.Id_Destino AND d.Baja = 0))
|
||||
OR (s.Destino != 'Distribuidores')
|
||||
)");
|
||||
}
|
||||
|
||||
if (idDestinoFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND s.Id_Destino = @IdDestino");
|
||||
sqlBuilder.Append(" AND Id_Destino = @IdDestino");
|
||||
parameters.Add("IdDestino", idDestinoFilter.Value);
|
||||
}
|
||||
if (idEmpresaFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND s.Id_Empresa = @IdEmpresa");
|
||||
sqlBuilder.Append(" AND Id_Empresa = @IdEmpresa");
|
||||
parameters.Add("IdEmpresa", idEmpresaFilter.Value);
|
||||
}
|
||||
sqlBuilder.Append(" ORDER BY s.Destino, s.Id_Empresa, s.Id_Destino;");
|
||||
sqlBuilder.Append(" ORDER BY Destino, Id_Empresa, Id_Destino;");
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters);
|
||||
|
||||
@@ -22,12 +22,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true)
|
||||
public async Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
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, d.Baja, d.FechaBaja,
|
||||
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
|
||||
@@ -44,11 +44,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
sqlBuilder.Append(" AND d.NroDoc LIKE @NroDocParam");
|
||||
parameters.Add("NroDocParam", $"%{nroDocFilter}%");
|
||||
}
|
||||
if (soloActivos.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND d.Baja = @BajaStatus ");
|
||||
parameters.Add("BajaStatus", !soloActivos.Value);
|
||||
}
|
||||
sqlBuilder.Append(" ORDER BY d.Nombre;");
|
||||
|
||||
try
|
||||
@@ -68,7 +63,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync(bool? soloActivos = true)
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync()
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
@@ -76,13 +71,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
FROM dbo.dist_dtDistribuidores
|
||||
WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (soloActivos.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Baja = @BajaStatus ");
|
||||
parameters.Add("BajaStatus", !soloActivos.Value);
|
||||
}
|
||||
|
||||
sqlBuilder.Append(" ORDER BY Nombre;");
|
||||
try
|
||||
{
|
||||
@@ -104,7 +92,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
const string sql = @"
|
||||
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, d.Baja, d.FechaBaja,
|
||||
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
|
||||
@@ -151,7 +139,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
const string sql = @"
|
||||
SELECT
|
||||
Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
||||
FROM dbo.dist_dtDistribuidores
|
||||
WHERE Id_Distribuidor = @IdParam";
|
||||
try
|
||||
@@ -235,10 +223,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
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, Baja, FechaBaja)
|
||||
INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad)
|
||||
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, INSERTED.Baja, INSERTED.FechaBaja
|
||||
VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, 0, NULL);";
|
||||
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);
|
||||
@@ -246,8 +234,8 @@ 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, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, 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
|
||||
{
|
||||
@@ -263,8 +251,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
TelefonoParam = inserted.Telefono,
|
||||
EmailParam = inserted.Email,
|
||||
LocalidadParam = inserted.Localidad,
|
||||
BajaParam = inserted.Baja,
|
||||
FechaBajaParam = inserted.FechaBaja,
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Creado"
|
||||
@@ -277,7 +263,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
var connection = transaction.Connection!;
|
||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
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.");
|
||||
@@ -289,8 +275,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
WHERE Id_Distribuidor = @IdDistribuidor;";
|
||||
const string sqlInsertHistorico = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, 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
|
||||
{
|
||||
@@ -306,8 +292,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
TelefonoParam = actual.Telefono,
|
||||
EmailParam = actual.Email,
|
||||
LocalidadParam = actual.Localidad,
|
||||
BajaParam = actual.Baja,
|
||||
FechaBajaParam = actual.FechaBaja,
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Actualizado"
|
||||
@@ -322,7 +306,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
var connection = transaction.Connection!;
|
||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
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.");
|
||||
|
||||
@@ -330,8 +314,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
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, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, 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
|
||||
{
|
||||
@@ -347,8 +331,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
TelefonoParam = actual.Telefono,
|
||||
EmailParam = actual.Email,
|
||||
LocalidadParam = actual.Localidad,
|
||||
BajaParam = actual.Baja,
|
||||
FechaBajaParam = actual.FechaBaja,
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Eliminado"
|
||||
@@ -358,47 +340,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction)
|
||||
{
|
||||
var connection = transaction.Connection!;
|
||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
|
||||
new { IdDistribuidorParam = id }, transaction);
|
||||
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado para dar de baja/alta.");
|
||||
|
||||
const string sqlUpdate = "UPDATE dbo.dist_dtDistribuidores SET Baja = @BajaParam, FechaBaja = @FechaBajaParam WHERE Id_Distribuidor = @IdDistribuidorParam;";
|
||||
const string sqlInsertHistorico = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaNuevaParam, @FechaBajaNuevaParam, @IdUsuarioParam, @FechaModParam, @TipoModHistParam);";
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
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,
|
||||
BajaNuevaParam = darDeBaja,
|
||||
FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null),
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModHistParam = (darDeBaja ? "Baja" : "Alta")
|
||||
}, transaction);
|
||||
|
||||
var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { BajaParam = darDeBaja, FechaBajaParam = (darDeBaja ? fechaBaja : null), IdDistribuidorParam = id }, transaction);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -72,13 +72,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// Inhabilitada la comprobacion de existencia previa por remito y tipo de movimiento
|
||||
// Pedido por Claudia Acosta el 18/11/2025
|
||||
// Motivo: El ex canillita Sergio Mazza opera como distribuidor y no utiliza remitos.
|
||||
// En el campo de remito se le asigna un numero aleatorio para cumplir con el requisito del sistema.
|
||||
// -------------------------------------------------------------------------------
|
||||
/*
|
||||
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");
|
||||
@@ -103,7 +96,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
return true; // Asumir que existe en caso de error para prevenir duplicados
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
public async Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction)
|
||||
{
|
||||
|
||||
@@ -8,17 +8,16 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public interface IDistribuidorRepository
|
||||
{
|
||||
Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true);
|
||||
Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter);
|
||||
Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id);
|
||||
Task<Distribuidor?> GetByIdSimpleAsync(int id); // Para uso interno en el servicio
|
||||
Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null);
|
||||
Task<bool> ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null);
|
||||
Task<bool> IsInUseAsync(int id); // Verificar en dist_EntradasSalidas, cue_PagosDistribuidor, dist_PorcPago
|
||||
Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync(bool? soloActivos = true);
|
||||
Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync();
|
||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
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);
|
||||
Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null);
|
||||
Task<IEnumerable<(EntradaSalidaDistHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
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 AND d.Baja = 0
|
||||
WHERE pp.Id_Publicacion = @IdPublicacionParam
|
||||
ORDER BY d.Nombre, pp.VigenciaD DESC";
|
||||
try
|
||||
{
|
||||
|
||||
@@ -15,9 +15,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
int? idEstadoBobina,
|
||||
string? remitoFilter,
|
||||
DateTime? fechaDesde,
|
||||
DateTime? fechaHasta,
|
||||
DateTime? fechaEstadoDesde,
|
||||
DateTime? fechaEstadoHasta);
|
||||
DateTime? fechaHasta);
|
||||
|
||||
Task<StockBobina?> GetByIdAsync(int idBobina);
|
||||
Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
|
||||
public async Task<IEnumerable<StockBobina>> GetAllAsync(
|
||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta)
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
@@ -69,16 +69,6 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam");
|
||||
parameters.Add("FechaHastaParam", fechaHasta.Value.Date);
|
||||
}
|
||||
if (fechaEstadoDesde.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND sb.FechaEstado >= @FechaEstadoDesdeParam");
|
||||
parameters.Add("FechaEstadoDesdeParam", fechaEstadoDesde.Value.Date);
|
||||
}
|
||||
if (fechaEstadoHasta.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND sb.FechaEstado <= @FechaEstadoHastaParam");
|
||||
parameters.Add("FechaEstadoHastaParam", fechaEstadoHasta.Value.Date);
|
||||
}
|
||||
|
||||
sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;");
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using GestionIntegral.Api.Services.Contables;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Middleware
|
||||
{
|
||||
// Centraliza el mapeo de excepciones semánticas a HTTP responses con cuerpo JSON estandarizado.
|
||||
// Va PRIMERO en el pipeline para catchear cualquier excepción que escape de los controllers/services.
|
||||
public class ExceptionHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionHandlerMiddleware> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public ExceptionHandlerMiddleware(RequestDelegate next, ILogger<ExceptionHandlerMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (BloqueoPorPeriodoCerradoException ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Bloqueo por período cerrado: cierre #{IdCierre} FechaCorte={FechaCorte:yyyy-MM-dd}. Path={Path}",
|
||||
ex.IdCierre, ex.FechaCorte, context.Request.Path);
|
||||
|
||||
await WriteJsonAsync(context, StatusCodes.Status409Conflict, new
|
||||
{
|
||||
codigo = "PERIODO_CERRADO_BLOQUEO_OPERACION",
|
||||
mensaje = ex.Message,
|
||||
idCierre = ex.IdCierre,
|
||||
fechaCorte = ex.FechaCorte.ToString("yyyy-MM-dd")
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Excepción no manejada. Path={Path}", context.Request.Path);
|
||||
|
||||
await WriteJsonAsync(context, StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
codigo = "ERROR_INTERNO",
|
||||
mensaje = "Ocurrió un error inesperado al procesar la solicitud."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static Task WriteJsonAsync(HttpContext context, int statusCode, object body)
|
||||
{
|
||||
if (context.Response.HasStarted)
|
||||
{
|
||||
// Si los headers ya se enviaron no podemos re-escribir el response. Solo loguear y salir.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
context.Response.Clear();
|
||||
context.Response.StatusCode = statusCode;
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
return context.Response.WriteAsync(JsonSerializer.Serialize(body, JsonOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
namespace GestionIntegral.Api.Models.Contables
|
||||
{
|
||||
public class CierreCuentaCorriente // Corresponde a cue_CierresCuentaCorriente
|
||||
{
|
||||
public int IdCierre { get; set; }
|
||||
public int IdDistribuidor { get; set; }
|
||||
public int IdEmpresa { get; set; }
|
||||
public DateTime FechaCorte { get; set; }
|
||||
public DateTime FechaCierre { get; set; }
|
||||
public decimal SaldoCierre { get; set; } // money en SQL, decimal en C#
|
||||
public string Estado { get; set; } = "Activo"; // 'Activo' | 'Anulado'
|
||||
public string? Justificacion { get; set; }
|
||||
public int IdUsuarioCierre { get; set; }
|
||||
public int? IdUsuarioAnula { get; set; }
|
||||
public DateTime? FechaAnulacion { get; set; }
|
||||
public string? JustificacionAnulacion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
namespace GestionIntegral.Api.Models.Contables
|
||||
{
|
||||
public class CierreCuentaCorrienteHistorico // Corresponde a cue_CierresCuentaCorriente_H
|
||||
{
|
||||
public int Id_Historial { get; set; }
|
||||
public int Id_Cierre { get; set; }
|
||||
public int Id_Distribuidor { get; set; }
|
||||
public int Id_Empresa { get; set; }
|
||||
public DateTime FechaCorte { get; set; }
|
||||
public DateTime FechaCierre { get; set; }
|
||||
public decimal SaldoCierre { get; set; }
|
||||
public string Estado { get; set; } = string.Empty;
|
||||
public string? Justificacion { get; set; }
|
||||
public int Id_Usuario_Cierre { get; set; }
|
||||
public int? Id_Usuario_Anula { get; set; }
|
||||
public DateTime? FechaAnulacion { get; set; }
|
||||
public string? Justificacion_Anulacion { get; set; }
|
||||
public string TipoMod { get; set; } = string.Empty; // 'Creacion' | 'Reapertura' | 'Modificacion'
|
||||
public int Id_Usuario_Mod { get; set; }
|
||||
public DateTime FechaMod { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,5 @@ namespace GestionIntegral.Api.Models.Distribucion
|
||||
public string? Telefono { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Localidad { get; set; }
|
||||
public bool Baja { get; set; } // Baja (bit, NOT NULL, DEFAULT 0)
|
||||
public DateTime? FechaBaja { get; set; } // FechaBaja (datetime2(0), NULL)
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ namespace GestionIntegral.Api.Models.Distribucion
|
||||
public string? Telefono { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Localidad { get; set; }
|
||||
public bool? Baja { get; set; }
|
||||
public DateTime? FechaBaja { get; set; }
|
||||
public int Id_Usuario { get; set; }
|
||||
public DateTime FechaMod { get; set; }
|
||||
public string TipoMod { get; set; } = string.Empty;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Auditoria
|
||||
{
|
||||
public class CierreCuentaCorrienteHistorialDto
|
||||
{
|
||||
public int Id_Historial { get; set; }
|
||||
public int Id_Cierre { get; set; }
|
||||
public int Id_Distribuidor { get; set; }
|
||||
public int Id_Empresa { get; set; }
|
||||
public DateTime FechaCorte { get; set; }
|
||||
public DateTime FechaCierre { get; set; }
|
||||
public decimal SaldoCierre { get; set; }
|
||||
public string Estado { get; set; } = string.Empty;
|
||||
public string? Justificacion { get; set; }
|
||||
public int Id_Usuario_Cierre { get; set; }
|
||||
public int? Id_Usuario_Anula { get; set; }
|
||||
public DateTime? FechaAnulacion { get; set; }
|
||||
public string? Justificacion_Anulacion { get; set; }
|
||||
|
||||
public string TipoMod { get; set; } = string.Empty; // 'Creacion' | 'Reapertura' | 'Modificacion'
|
||||
public int Id_Usuario_Mod { get; set; }
|
||||
public string NombreUsuarioModifico { get; set; } = string.Empty;
|
||||
public DateTime FechaMod { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
@@ -20,16 +19,10 @@ namespace GestionIntegral.Api.Dtos.Contables
|
||||
[Required(ErrorMessage = "El monto del ajuste es obligatorio.")]
|
||||
// Permitir montos negativos para disminuir deuda o positivos para aumentarla
|
||||
// No se usa Range aquí para permitir ambos signos. La validación de que no sea cero se puede hacer en el servicio.
|
||||
public decimal MontoAjuste { get; set; }
|
||||
public decimal MontoAjuste { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La justificación del ajuste es obligatoria.")]
|
||||
[StringLength(250, MinimumLength = 5, ErrorMessage = "La justificación debe tener entre 5 y 250 caracteres.")]
|
||||
public string Justificacion { get; set; } = string.Empty;
|
||||
|
||||
// Fecha lógica de la operación. Se valida contra el último cierre vigente
|
||||
// del par (Distribuidor + Empresa) para bloquear ajustes en períodos cerrados.
|
||||
// Distinta de FechaAjuste, que es el momento de ejecución del ajuste en el sistema.
|
||||
[Required(ErrorMessage = "La fecha de operación es obligatoria.")]
|
||||
public DateTime FechaOperacion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
{
|
||||
public class CierreCuentaCorrienteDto
|
||||
{
|
||||
public int IdCierre { get; set; }
|
||||
public int IdDistribuidor { get; set; }
|
||||
public string NombreDistribuidor { get; set; } = string.Empty;
|
||||
public int IdEmpresa { get; set; }
|
||||
public string NombreEmpresa { get; set; } = string.Empty;
|
||||
public string FechaCorte { get; set; } = string.Empty; // yyyy-MM-dd
|
||||
public DateTime FechaCierre { get; set; }
|
||||
public decimal SaldoCierre { get; set; }
|
||||
public string Estado { get; set; } = string.Empty;
|
||||
public string? Justificacion { get; set; }
|
||||
public int IdUsuarioCierre { get; set; }
|
||||
public string NombreUsuarioCierre { get; set; } = string.Empty;
|
||||
public int? IdUsuarioAnula { get; set; }
|
||||
public string? NombreUsuarioAnula { get; set; }
|
||||
public DateTime? FechaAnulacion { get; set; }
|
||||
public string? JustificacionAnulacion { get; set; }
|
||||
public bool EsUltimoVigente { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
{
|
||||
public class CrearCierreDto
|
||||
{
|
||||
[Required(ErrorMessage = "El distribuidor es obligatorio.")]
|
||||
[Range(1, int.MaxValue, ErrorMessage = "ID de Distribuidor inválido.")]
|
||||
public int IdDistribuidor { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La empresa es obligatoria.")]
|
||||
[Range(1, int.MaxValue, ErrorMessage = "ID de Empresa inválido.")]
|
||||
public int IdEmpresa { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La fecha de corte es obligatoria.")]
|
||||
public DateTime FechaCorte { get; set; }
|
||||
|
||||
[StringLength(500, ErrorMessage = "La justificación no puede superar los 500 caracteres.")]
|
||||
public string? Justificacion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
{
|
||||
public class ReabrirCierreDto
|
||||
{
|
||||
[Required(ErrorMessage = "La justificación es obligatoria al reabrir un cierre.")]
|
||||
[StringLength(500, MinimumLength = 10, ErrorMessage = "La justificación debe tener entre 10 y 500 caracteres.")]
|
||||
public string Justificacion { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
{
|
||||
public class UltimoCierreDto
|
||||
{
|
||||
public int IdCierre { get; set; }
|
||||
public string FechaCorte { get; set; } = string.Empty; // yyyy-MM-dd
|
||||
public decimal SaldoCierre { get; set; }
|
||||
public string Estado { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,5 @@ namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
public string? Telefono { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Localidad { get; set; }
|
||||
public bool Baja { get; set; }
|
||||
public DateTime? FechaBaja { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class ToggleBajaDistribuidorDto
|
||||
{
|
||||
public bool DarDeBaja { get; set; }
|
||||
public DateTime? FechaBaja { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class BobinaLoteDetalleDto
|
||||
{
|
||||
public int IdTipoBobina { get; set; }
|
||||
public string NroBobina { get; set; } = string.Empty;
|
||||
public int Peso { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class CreateStockBobinaLoteDto
|
||||
{
|
||||
[Required]
|
||||
public int IdPlanta { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(15)]
|
||||
public string Remito { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public DateTime FechaRemito { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(1, ErrorMessage = "Debe ingresar al menos una bobina.")]
|
||||
public List<BobinaLoteDetalleDto> Bobinas { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class UpdateFechaRemitoLoteDto
|
||||
{
|
||||
[Required]
|
||||
public int IdPlanta { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Remito { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime FechaRemitoActual { get; set; } // Para seguridad, nos aseguramos de cambiar el lote correcto
|
||||
|
||||
[Required]
|
||||
public DateTime NuevaFechaRemito { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,8 @@ namespace GestionIntegral.Api.Dtos.Reportes
|
||||
public IEnumerable<BalanceCuentaDistDto> EntradasSalidas { get; set; } = new List<BalanceCuentaDistDto>();
|
||||
public IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos { get; set; } = new List<BalanceCuentaDebCredDto>();
|
||||
public IEnumerable<BalanceCuentaPagosDto> Pagos { get; set; } = new List<BalanceCuentaPagosDto>();
|
||||
public string? NombreDistribuidor { get; set; }
|
||||
public string? NombreEmpresa { get; set; }
|
||||
|
||||
// Saldo a la fecha desde elegida en el filtro:
|
||||
// - Si existe cierre con FechaCorte < FechaDesde: SaldoCierre + movimientos netos entre fechaCierre+1 y fechaDesde-1.
|
||||
// - Sin cierres previos: 0.
|
||||
public decimal SaldoInicial { get; set; }
|
||||
public IEnumerable<SaldoDto> Saldos { get; set; } = new List<SaldoDto>(); // O podría ser SaldoDto SaldoActual si siempre es uno
|
||||
public string? NombreDistribuidor { get; set; } // Para el título del reporte
|
||||
public string? NombreEmpresa { get; set; } // Para el título del reporte
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,9 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
public IEnumerable<BalanceCuentaDistDto> Movimientos { get; set; } = new List<BalanceCuentaDistDto>();
|
||||
public IEnumerable<BalanceCuentaPagosDto> Pagos { get; set; } = new List<BalanceCuentaPagosDto>();
|
||||
public IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos { get; set; } = new List<BalanceCuentaDebCredDto>();
|
||||
|
||||
// Saldo inicial del período: snapshot del último cierre + movimientos netos hasta fechaDesde.
|
||||
// 0 si no hay cierre previo.
|
||||
public decimal SaldoInicial { get; set; }
|
||||
|
||||
// Saldo real de la cuenta, se muestra al final sin usarse en cálculos intermedios.
|
||||
public decimal SaldoDeCuenta { get; set; }
|
||||
|
||||
// --- Parámetros del reporte ---
|
||||
public string NombreDistribuidor { get; set; } = string.Empty;
|
||||
@@ -25,8 +24,6 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
public decimal TotalMovimientos => Movimientos.Sum(m => m.Debe - m.Haber);
|
||||
public decimal TotalPagos => Pagos.Sum(p => p.Debe - p.Haber);
|
||||
public decimal TotalDebitosCreditos => DebitosCreditos.Sum(d => d.Debe - d.Haber);
|
||||
|
||||
// Saldo Final = Saldo Inicial + suma neta del período (Debe - Haber por sección).
|
||||
public decimal SaldoFinal => SaldoInicial + TotalMovimientos + TotalPagos + TotalDebitosCreditos;
|
||||
public decimal TotalPeriodo => TotalMovimientos + TotalPagos + TotalDebitosCreditos;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
public string FechaDesde { get; set; } = string.Empty;
|
||||
public string FechaHasta { get; set; } = string.Empty;
|
||||
public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy");
|
||||
|
||||
|
||||
public ListadoDistribucionCanillasSimpleDto TotalesDetalleDiario
|
||||
{
|
||||
get
|
||||
@@ -29,23 +29,7 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public decimal PorcentajeDevolucionGeneral
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PromediosPorDia == null || !PromediosPorDia.Any()) return 0;
|
||||
|
||||
var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
|
||||
var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant);
|
||||
|
||||
if (totalPonderadoLlevados == 0) return 0;
|
||||
|
||||
// Calculamos el porcentaje usando los totales ponderados para máxima precisión como lo hace el frontend.
|
||||
return (decimal)totalPonderadoDevueltos * 100 / totalPonderadoLlevados;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- PROPIEDAD PARA LA FILA "GENERAL" ---
|
||||
public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral
|
||||
{
|
||||
@@ -53,27 +37,20 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
{
|
||||
if (PromediosPorDia == null || !PromediosPorDia.Any()) return null;
|
||||
|
||||
// Sumamos los totales ponderados para cada columna
|
||||
var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
|
||||
var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant);
|
||||
var totalPonderadoVentas = PromediosPorDia.Sum(p => p.Promedio_Ventas * p.Cant);
|
||||
// Sumamos los totales, no promediamos los promedios
|
||||
var totalLlevados = PromediosPorDia.Sum(p => p.Llevados);
|
||||
var totalDevueltos = PromediosPorDia.Sum(p => p.Devueltos);
|
||||
var totalDias = PromediosPorDia.Sum(p => p.Cant);
|
||||
|
||||
if (totalDias == 0) return null;
|
||||
|
||||
// Usamos Math.Round para un redondeo matemático estándar antes de la conversión.
|
||||
// MidpointRounding.AwayFromZero asegura que .5 se redondee hacia arriba, igual que en JavaScript.
|
||||
var promGeneralLlevados = (int)Math.Round((decimal)totalPonderadoLlevados / totalDias, MidpointRounding.AwayFromZero);
|
||||
var promGeneralDevueltos = (int)Math.Round((decimal)totalPonderadoDevueltos / totalDias, MidpointRounding.AwayFromZero);
|
||||
var promGeneralVentas = (int)Math.Round((decimal)totalPonderadoVentas / totalDias, MidpointRounding.AwayFromZero);
|
||||
|
||||
return new ListadoDistribucionCanillasPromedioDiaDto
|
||||
{
|
||||
Dia = "General",
|
||||
Cant = totalDias,
|
||||
Promedio_Llevados = promGeneralLlevados,
|
||||
Promedio_Devueltos = promGeneralDevueltos,
|
||||
Promedio_Ventas = promGeneralVentas
|
||||
Promedio_Llevados = totalLlevados / totalDias,
|
||||
Promedio_Devueltos = totalDevueltos / totalDias,
|
||||
Promedio_Ventas = (totalLlevados - totalDevueltos) / totalDias
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,26 +20,26 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PromediosPorDia == null || !PromediosPorDia.Any())
|
||||
if (DetalleDiario == null || !DetalleDiario.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var promediosValidos = PromediosPorDia.Where(p => p.Dia != "General").ToList();
|
||||
if (!promediosValidos.Any()) return null;
|
||||
var countPromedios = promediosValidos.Count;
|
||||
var sumPromLlevados = promediosValidos.Sum(p => p.Promedio_Llevados ?? 0);
|
||||
var sumPromDevueltos = promediosValidos.Sum(p => p.Promedio_Devueltos ?? 0);
|
||||
var sumPromVentas = promediosValidos.Sum(p => p.Promedio_Ventas ?? 0);
|
||||
|
||||
var diasConDatos = DetalleDiario.Count(d => (d.Llevados ?? 0) > 0);
|
||||
if (diasConDatos == 0) return null;
|
||||
|
||||
var totalLlevados = DetalleDiario.Sum(d => d.Llevados ?? 0);
|
||||
var totalDevueltos = DetalleDiario.Sum(d => d.Devueltos ?? 0);
|
||||
|
||||
return new ListadoDistribucionDistPromedioDiaDto
|
||||
{
|
||||
Dia = "General",
|
||||
Cant = promediosValidos.Sum(p => p.Cant ?? 0),
|
||||
Promedio_Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Promedio_Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Promedio_Ventas = (int)Math.Round((decimal)sumPromVentas / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero)
|
||||
Cant = diasConDatos,
|
||||
Promedio_Llevados = totalLlevados / diasConDatos,
|
||||
Promedio_Devueltos = totalDevueltos / diasConDatos,
|
||||
Promedio_Ventas = (totalLlevados - totalDevueltos) / diasConDatos,
|
||||
Llevados = totalLlevados, // Guardamos el total para el cálculo del %
|
||||
Devueltos = totalDevueltos // Guardamos el total para el cálculo del %
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ using GestionIntegral.Api.Services.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Comunicaciones;
|
||||
using GestionIntegral.Api.Services.Comunicaciones;
|
||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||
using GestionIntegral.Api.Middleware;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -97,12 +96,6 @@ builder.Services.AddScoped<ICambioParadaRepository, CambioParadaRepository>();
|
||||
builder.Services.AddScoped<ICambioParadaService, CambioParadaService>();
|
||||
// Servicio de Saldos
|
||||
builder.Services.AddScoped<ISaldoService, SaldoService>();
|
||||
// Cierre de Cuenta Corriente de Distribuidor
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddScoped<ICierreCuentaCorrienteRepository, CierreCuentaCorrienteRepository>();
|
||||
builder.Services.AddScoped<ICierreCuentaCorrienteService, CierreCuentaCorrienteService>();
|
||||
// Validador de período cerrado: SINGLETON porque mantiene cache en memoria de IMemoryCache que debe ser compartido entre requests.
|
||||
builder.Services.AddSingleton<IPeriodoCerradoValidator, PeriodoCerradoValidator>();
|
||||
// Repositorios de Reportes
|
||||
builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
|
||||
// Servicios de Reportes
|
||||
@@ -276,10 +269,6 @@ if (app.Environment.IsDevelopment())
|
||||
// Comenta o elimina la siguiente línea si SÓLO usas http://localhost:5183
|
||||
//app.UseHttpsRedirection();
|
||||
|
||||
// Middleware global de excepciones — debe ir TEMPRANO en el pipeline para catchear cualquier excepción
|
||||
// que escape de los controllers/services. Mapea BloqueoPorPeriodoCerradoException → 409 con cuerpo JSON estandarizado.
|
||||
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
||||
|
||||
app.UseCors(MyAllowSpecificOrigins);
|
||||
|
||||
app.UseAuthentication(); // Debe ir ANTES de UseAuthorization
|
||||
|
||||
@@ -22,16 +22,13 @@ namespace GestionIntegral.Api.Services.Anomalia
|
||||
public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync()
|
||||
{
|
||||
// Apunta a la nueva tabla genérica 'Sistema_Alertas'
|
||||
//var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
||||
var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
||||
try
|
||||
{
|
||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||
{
|
||||
/*
|
||||
var alertas = await connection.QueryAsync<AlertaGenericaDto>(query);
|
||||
return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
|
||||
*/
|
||||
return Enumerable.Empty<AlertaGenericaDto>();
|
||||
return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -43,20 +40,17 @@ namespace GestionIntegral.Api.Services.Anomalia
|
||||
|
||||
public async Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta)
|
||||
{
|
||||
//var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
||||
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
||||
try
|
||||
{
|
||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||
{
|
||||
/*
|
||||
var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta });
|
||||
if (result > 0)
|
||||
{
|
||||
return (true, null);
|
||||
}
|
||||
return (false, "La alerta no fue encontrada o ya estaba marcada.");
|
||||
*/
|
||||
return (true, null); // Retornar éxito silencioso por ahora
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -68,18 +62,15 @@ namespace GestionIntegral.Api.Services.Anomalia
|
||||
|
||||
public async Task<(bool Exito, string? Error)> MarcarGrupoComoLeidoAsync(string tipoAlerta, int idEntidad)
|
||||
{
|
||||
//var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
||||
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
||||
try
|
||||
{
|
||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||
{
|
||||
/*
|
||||
var result = await connection.ExecuteAsync(query, new { TipoAlerta = tipoAlerta, IdEntidad = idEntidad });
|
||||
// No es un error si no se actualizan filas (puede que no hubiera ninguna para ese grupo)
|
||||
_logger.LogInformation("Marcadas como leídas {Count} alertas para Tipo: {Tipo}, EntidadID: {IdEntidad}", result, tipoAlerta, idEntidad);
|
||||
return (true, null);
|
||||
*/
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
// Se lanza cuando un C/U/D intenta afectar la cuenta corriente de un distribuidor en una fecha
|
||||
// que cae dentro de un período cerrado (cue_CierresCuentaCorriente con Estado='Activo').
|
||||
// Mapea al código de error PERIODO_CERRADO_BLOQUEO_OPERACION (HTTP 409).
|
||||
public class BloqueoPorPeriodoCerradoException : Exception
|
||||
{
|
||||
public int IdCierre { get; }
|
||||
public DateTime FechaCorte { get; }
|
||||
|
||||
public BloqueoPorPeriodoCerradoException(int idCierre, DateTime fechaCorte)
|
||||
: base($"El período está cerrado al {fechaCorte:dd/MM/yyyy} (cierre #{idCierre}). No se permiten modificaciones sobre fechas anteriores o iguales a la fecha de corte.")
|
||||
{
|
||||
IdCierre = idCierre;
|
||||
FechaCorte = fechaCorte;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Contables;
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Data.Repositories.Reportes;
|
||||
using GestionIntegral.Api.Dtos.Auditoria;
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using GestionIntegral.Api.Models.Contables;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
public class CierreCuentaCorrienteService : ICierreCuentaCorrienteService
|
||||
{
|
||||
private readonly ICierreCuentaCorrienteRepository _cierreRepo;
|
||||
private readonly IDistribuidorRepository _distRepo;
|
||||
private readonly IEmpresaRepository _empresaRepo;
|
||||
private readonly IPeriodoCerradoValidator _validator;
|
||||
private readonly IReportesRepository _reportesRepo;
|
||||
private readonly DbConnectionFactory _cf;
|
||||
private readonly ILogger<CierreCuentaCorrienteService> _logger;
|
||||
|
||||
// Retry de deadlocks (SqlException.Number == 1205) para la transacción Serializable de Crear cierre.
|
||||
private static readonly int[] DeadlockBackoffMs = { 50, 150, 400 };
|
||||
|
||||
// Sentinela para "sin límite inferior" en el cálculo histórico — anterior a cualquier dato real.
|
||||
private static readonly DateTime InicioHistoria = new DateTime(1900, 1, 1);
|
||||
|
||||
public CierreCuentaCorrienteService(
|
||||
ICierreCuentaCorrienteRepository cierreRepo,
|
||||
IDistribuidorRepository distRepo,
|
||||
IEmpresaRepository empresaRepo,
|
||||
IPeriodoCerradoValidator validator,
|
||||
IReportesRepository reportesRepo,
|
||||
DbConnectionFactory cf,
|
||||
ILogger<CierreCuentaCorrienteService> logger)
|
||||
{
|
||||
_cierreRepo = cierreRepo;
|
||||
_distRepo = distRepo;
|
||||
_empresaRepo = empresaRepo;
|
||||
_validator = validator;
|
||||
_reportesRepo = reportesRepo;
|
||||
_cf = cf;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> CrearCierreAsync(CrearCierreDto dto, int idUsuario)
|
||||
{
|
||||
// Validaciones pre-transacción (no requieren lock).
|
||||
if (dto.FechaCorte.Date > DateTime.Today)
|
||||
return (null, "CIERRE_FECHA_FUTURA", "La fecha de corte no puede ser futura.");
|
||||
|
||||
var distribuidor = await _distRepo.GetByIdSimpleAsync(dto.IdDistribuidor);
|
||||
if (distribuidor == null)
|
||||
return (null, "CIERRE_DISTRIBUIDOR_BAJA", "El distribuidor especificado no existe.");
|
||||
if (distribuidor.Baja)
|
||||
return (null, "CIERRE_DISTRIBUIDOR_BAJA", "El distribuidor está dado de baja.");
|
||||
|
||||
if (await _empresaRepo.GetByIdAsync(dto.IdEmpresa) == null)
|
||||
return (null, "CIERRE_EMPRESA_INEXISTENTE", "La empresa especificada no existe.");
|
||||
|
||||
// Retry loop ante deadlock 1205. Los range locks de Serializable sobre las queries de movimientos
|
||||
// pueden chocar con transacciones concurrentes que insertan pagos/notas/ajustes/movimientos.
|
||||
for (int attempt = 0; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CrearCierreInternalAsync(dto, idUsuario);
|
||||
}
|
||||
catch (SqlException ex) when (ex.Number == 1205 && attempt < DeadlockBackoffMs.Length)
|
||||
{
|
||||
_logger.LogWarning("Deadlock 1205 al crear cierre Dist={IdDist} Emp={IdEmp}, intento {Attempt}. Reintentando.", dto.IdDistribuidor, dto.IdEmpresa, attempt + 1);
|
||||
await Task.Delay(DeadlockBackoffMs[attempt]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error inesperado al crear cierre Dist={IdDist} Emp={IdEmp}.", dto.IdDistribuidor, dto.IdEmpresa);
|
||||
return (null, "CIERRE_ERROR_INTERNO", $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> CrearCierreInternalAsync(CrearCierreDto dto, int idUsuario)
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
|
||||
using var transaction = connection.BeginTransaction(IsolationLevel.Serializable);
|
||||
try
|
||||
{
|
||||
// 1. Lookup último cierre dentro de la transacción Serializable.
|
||||
var ultimo = await _cierreRepo.GetUltimoCierreVigenteAsync(dto.IdDistribuidor, dto.IdEmpresa, transaction);
|
||||
if (ultimo != null && dto.FechaCorte.Date <= ultimo.FechaCorte.Date)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (null, "CIERRE_FECHA_ANTERIOR_A_ULTIMO",
|
||||
$"Existe un cierre vigente al {ultimo.FechaCorte:dd/MM/yyyy}. La fecha de corte debe ser posterior.");
|
||||
}
|
||||
|
||||
// 2. Calcular SaldoCierre por suma de movimientos (la tabla cue_Saldos NO se consulta — queda paralela
|
||||
// para conciliación y su lógica original es independiente).
|
||||
// Si hay cierre anterior: desde (ultimo.FechaCorte + 1 día) hasta dto.FechaCorte INCLUSIVE.
|
||||
// Si no hay cierre anterior: desde InicioHistoria hasta dto.FechaCorte INCLUSIVE (histórico completo).
|
||||
DateTime desde = ultimo != null ? ultimo.FechaCorte.Date.AddDays(1) : InicioHistoria;
|
||||
DateTime hasta = dto.FechaCorte.Date;
|
||||
|
||||
decimal sumaMovimientos = await CalcularSaldoEntreFechasAsync(
|
||||
dto.IdDistribuidor, dto.IdEmpresa, desde, hasta, connection, transaction);
|
||||
|
||||
decimal saldoBase = ultimo?.SaldoCierre ?? 0m;
|
||||
decimal saldoCierre = saldoBase + sumaMovimientos;
|
||||
|
||||
// 3. Crear el cierre + entry _H ('Creacion') dentro de la misma transacción.
|
||||
var nuevo = new CierreCuentaCorriente
|
||||
{
|
||||
IdDistribuidor = dto.IdDistribuidor,
|
||||
IdEmpresa = dto.IdEmpresa,
|
||||
FechaCorte = dto.FechaCorte.Date,
|
||||
FechaCierre = DateTime.Now,
|
||||
SaldoCierre = saldoCierre,
|
||||
Estado = "Activo",
|
||||
Justificacion = dto.Justificacion,
|
||||
IdUsuarioCierre = idUsuario
|
||||
};
|
||||
|
||||
int idCierre = await _cierreRepo.CreateAsync(nuevo, idUsuario, transaction);
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Cierre #{Id} creado Dist={IdDist} Emp={IdEmp} FechaCorte={FechaCorte} Saldo={Saldo} (base={Base}, suma={Suma})",
|
||||
idCierre, dto.IdDistribuidor, dto.IdEmpresa, dto.FechaCorte, saldoCierre, saldoBase, sumaMovimientos);
|
||||
|
||||
// 4. Invalidar cache del validador post-commit.
|
||||
_validator.InvalidarCache(dto.IdDistribuidor, dto.IdEmpresa);
|
||||
|
||||
// 5. Mapear DTO completo (con nombres) para la respuesta.
|
||||
var dtoCreado = (await _cierreRepo.GetAllAsync(dto.IdDistribuidor, dto.IdEmpresa, null, null, null))
|
||||
.FirstOrDefault(c => c.IdCierre == idCierre);
|
||||
|
||||
return (dtoCreado, null, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearCierreInternalAsync."); }
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> ReabrirCierreAsync(int idCierre, ReabrirCierreDto dto, int idUsuario, bool esSuperAdmin)
|
||||
{
|
||||
if (!esSuperAdmin)
|
||||
return (null, "CIERRE_PERMISO_DENEGADO", "Sólo SuperAdmin puede reabrir cierres.");
|
||||
|
||||
// Re-validar justificación a nivel service (defensa en profundidad — el DTO ya tiene DataAnnotations).
|
||||
if (string.IsNullOrWhiteSpace(dto.Justificacion) || dto.Justificacion.Trim().Length < 10)
|
||||
return (null, "CIERRE_JUSTIFICACION_OBLIGATORIA", "La justificación es obligatoria y debe tener al menos 10 caracteres.");
|
||||
|
||||
using var connection = _cf.CreateConnection();
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
|
||||
using var transaction = connection.BeginTransaction(IsolationLevel.Serializable);
|
||||
try
|
||||
{
|
||||
var cierre = await _cierreRepo.GetByIdAsync(idCierre);
|
||||
if (cierre == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (null, "CIERRE_NO_ENCONTRADO", "El cierre solicitado no existe.");
|
||||
}
|
||||
|
||||
if (cierre.Estado != "Activo")
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (null, "CIERRE_YA_ANULADO", "El cierre ya está anulado.");
|
||||
}
|
||||
|
||||
bool hayPosterior = await _cierreRepo.ExisteCierrePosteriorVigenteAsync(
|
||||
cierre.IdDistribuidor, cierre.IdEmpresa, cierre.FechaCorte, idCierre, transaction);
|
||||
if (hayPosterior)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (null, "CIERRE_HAY_POSTERIORES_VIGENTES",
|
||||
"Existe un cierre posterior vigente. Reabrir primero los cierres posteriores en cascada manual.");
|
||||
}
|
||||
|
||||
bool anulado = await _cierreRepo.AnularAsync(idCierre, idUsuario, dto.Justificacion.Trim(), idUsuario, transaction);
|
||||
if (!anulado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
// Carrera concurrente: alguien lo anuló entre el GetByIdAsync y el UPDATE atómico.
|
||||
return (null, "CIERRE_YA_ANULADO", "El cierre fue anulado concurrentemente por otra operación.");
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Cierre #{Id} reabierto (anulado) por Usuario {IdUsuario}.", idCierre, idUsuario);
|
||||
|
||||
_validator.InvalidarCache(cierre.IdDistribuidor, cierre.IdEmpresa);
|
||||
|
||||
var dtoActualizado = (await _cierreRepo.GetAllAsync(cierre.IdDistribuidor, cierre.IdEmpresa, null, null, null))
|
||||
.FirstOrDefault(c => c.IdCierre == idCierre);
|
||||
return (dtoActualizado, null, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ReabrirCierreAsync."); }
|
||||
_logger.LogError(ex, "Error inesperado al reabrir cierre #{Id}.", idCierre);
|
||||
return (null, "CIERRE_ERROR_INTERNO", $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> CalcularSaldoInicialReporteAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde)
|
||||
{
|
||||
// 1. Buscar último cierre vigente con FechaCorte ESTRICTAMENTE menor a fechaDesde.
|
||||
// Si no hay cierre previo, saldoInicial = 0 (comportamiento "como antes" del reporte).
|
||||
const string sqlUltimoCierre = @"
|
||||
SELECT TOP 1 FechaCorte, SaldoCierre
|
||||
FROM dbo.cue_CierresCuentaCorriente
|
||||
WHERE Id_Distribuidor = @IdDist
|
||||
AND Id_Empresa = @IdEmp
|
||||
AND Estado = 'Activo'
|
||||
AND FechaCorte < @FechaDesde
|
||||
ORDER BY FechaCorte DESC, Id_Cierre DESC;";
|
||||
|
||||
using var connection = _cf.CreateConnection();
|
||||
var ultimo = await connection.QuerySingleOrDefaultAsync<(DateTime FechaCorte, decimal SaldoCierre)?>(
|
||||
sqlUltimoCierre,
|
||||
new { IdDist = idDistribuidor, IdEmp = idEmpresa, FechaDesde = fechaDesde.Date });
|
||||
|
||||
if (ultimo == null) return 0m;
|
||||
|
||||
// 2. Sumar movimientos netos entre (fechaCorte + 1 día) y (fechaDesde - 1 día) — INCLUSIVE en ambos extremos.
|
||||
// El SaldoCierre ya incluye los movimientos hasta fechaCorte, así que arrancamos al día siguiente.
|
||||
DateTime desdeMov = ultimo.Value.FechaCorte.Date.AddDays(1);
|
||||
DateTime hastaMov = fechaDesde.Date.AddDays(-1);
|
||||
|
||||
decimal sumaMovs = await CalcularSaldoEntreFechasAsync(
|
||||
idDistribuidor, idEmpresa, desdeMov, hastaMov, connection, null);
|
||||
|
||||
return ultimo.Value.SaldoCierre + sumaMovs;
|
||||
}
|
||||
|
||||
// Helper unificado de suma neta de movimientos en rango [desde, hasta] (ambos INCLUSIVE).
|
||||
// Si se pasa una transacción activa, las queries corren bajo ese contexto y los range locks
|
||||
// de Serializable cubren la consistencia con respecto a inserts concurrentes en las tablas de movimientos.
|
||||
private async Task<decimal> CalcularSaldoEntreFechasAsync(
|
||||
int idDistribuidor, int idEmpresa,
|
||||
DateTime desde, DateTime hasta,
|
||||
IDbConnection connection,
|
||||
IDbTransaction? transaction)
|
||||
{
|
||||
if (desde > hasta) return 0m;
|
||||
|
||||
// Pagos + Notas C/D destino Distribuidores + Ajustes manuales destino Distribuidores en una sola query.
|
||||
const string sqlSumaTransaccionales = @"
|
||||
SELECT ISNULL(SUM(MontoNeto), 0) AS Total
|
||||
FROM (
|
||||
SELECT CASE WHEN TipoMovimiento = 'Recibido' THEN -CAST(Monto AS DECIMAL(18,4)) ELSE CAST(Monto AS DECIMAL(18,4)) END AS MontoNeto
|
||||
FROM dbo.cue_PagosDistribuidor
|
||||
WHERE Id_Distribuidor = @IdDist AND Id_Empresa = @IdEmp
|
||||
AND Fecha >= @Desde AND Fecha <= @Hasta
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT CASE WHEN Tipo = 'Credito' THEN -CAST(Monto AS DECIMAL(18,4)) ELSE CAST(Monto AS DECIMAL(18,4)) END
|
||||
FROM dbo.cue_CreditosDebitos
|
||||
WHERE Destino = 'Distribuidores' AND Id_Destino = @IdDist AND Id_Empresa = @IdEmp
|
||||
AND Fecha >= @Desde AND Fecha <= @Hasta
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT CAST(MontoAjuste AS DECIMAL(18,4))
|
||||
FROM dbo.cue_SaldoAjustesHistorial
|
||||
WHERE Destino = 'Distribuidores' AND Id_Destino = @IdDist AND Id_Empresa = @IdEmp
|
||||
AND FechaOperacion >= @Desde AND FechaOperacion <= @Hasta
|
||||
) m;";
|
||||
|
||||
decimal sumaTransaccional = await connection.ExecuteScalarAsync<decimal>(
|
||||
sqlSumaTransaccionales,
|
||||
new { IdDist = idDistribuidor, IdEmp = idEmpresa, Desde = desde, Hasta = hasta },
|
||||
transaction);
|
||||
|
||||
// EntradasSalidas: SP existente (single source of truth de la fórmula CalcularMontoMovimiento que es private en EntradaSalidaDistService).
|
||||
var movimientosES = await _reportesRepo.GetBalanceCuentaDistEntradaSalidaPorEmpresaAsync(
|
||||
idDistribuidor, idEmpresa, desde, hasta);
|
||||
|
||||
decimal sumaEntradasSalidas = movimientosES?.Sum(m => m.Debe - m.Haber) ?? 0m;
|
||||
|
||||
return sumaTransaccional + sumaEntradasSalidas;
|
||||
}
|
||||
|
||||
public async Task<CierreCuentaCorrienteDto?> GetByIdAsync(int idCierre)
|
||||
{
|
||||
var cierre = await _cierreRepo.GetByIdAsync(idCierre);
|
||||
if (cierre == null) return null;
|
||||
return (await _cierreRepo.GetAllAsync(cierre.IdDistribuidor, cierre.IdEmpresa, null, null, null))
|
||||
.FirstOrDefault(c => c.IdCierre == idCierre);
|
||||
}
|
||||
|
||||
public async Task<UltimoCierreDto?> GetUltimoVigenteAsync(int idDistribuidor, int idEmpresa)
|
||||
{
|
||||
var ultimo = await _cierreRepo.GetUltimoCierreVigenteAsync(idDistribuidor, idEmpresa);
|
||||
if (ultimo == null) return null;
|
||||
return new UltimoCierreDto
|
||||
{
|
||||
IdCierre = ultimo.IdCierre,
|
||||
FechaCorte = ultimo.FechaCorte.ToString("yyyy-MM-dd"),
|
||||
SaldoCierre = ultimo.SaldoCierre,
|
||||
Estado = ultimo.Estado
|
||||
};
|
||||
}
|
||||
|
||||
public Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta)
|
||||
=> _cierreRepo.GetAllAsync(idDistribuidor, idEmpresa, estado, fechaCorteDesde, fechaCorteHasta);
|
||||
|
||||
public Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre)
|
||||
=> _cierreRepo.GetHistorialAsync(idCierre);
|
||||
|
||||
public Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idCierreAfectado)
|
||||
=> _cierreRepo.ObtenerHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idCierreAfectado);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using GestionIntegral.Api.Dtos.Auditoria;
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
public interface ICierreCuentaCorrienteService
|
||||
{
|
||||
// Devuelve (Cierre, ErrorCode, ErrorMessage).
|
||||
// ErrorCode = código semántico (CIERRE_FECHA_FUTURA, CIERRE_FECHA_ANTERIOR_A_ULTIMO, CIERRE_DISTRIBUIDOR_BAJA, CIERRE_EMPRESA_INEXISTENTE, CIERRE_ERROR_INTERNO).
|
||||
Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> CrearCierreAsync(CrearCierreDto dto, int idUsuario);
|
||||
|
||||
// ErrorCode = CIERRE_NO_ENCONTRADO, CIERRE_YA_ANULADO, CIERRE_HAY_POSTERIORES_VIGENTES, CIERRE_PERMISO_DENEGADO, CIERRE_ERROR_INTERNO.
|
||||
Task<(CierreCuentaCorrienteDto? Cierre, string? ErrorCode, string? ErrorMessage)> ReabrirCierreAsync(int idCierre, ReabrirCierreDto dto, int idUsuario, bool esSuperAdmin);
|
||||
|
||||
// Saldo inicial del reporte de cuenta corriente para el filtro fechaDesde:
|
||||
// - Sin cierre previo: 0.
|
||||
// - Con cierre con FechaCorte < fechaDesde: SaldoCierre + sumaNeta(fechaCierre+1 .. fechaDesde-1) sobre 4 fuentes.
|
||||
Task<decimal> CalcularSaldoInicialReporteAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde);
|
||||
|
||||
Task<CierreCuentaCorrienteDto?> GetByIdAsync(int idCierre);
|
||||
Task<UltimoCierreDto?> GetUltimoVigenteAsync(int idDistribuidor, int idEmpresa);
|
||||
Task<IEnumerable<CierreCuentaCorrienteDto>> GetAllAsync(
|
||||
int? idDistribuidor, int? idEmpresa, string? estado,
|
||||
DateTime? fechaCorteDesde, DateTime? fechaCorteHasta);
|
||||
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> GetHistorialAsync(int idCierre);
|
||||
|
||||
// Auditoría general — filtros cruzados sobre todos los cierres.
|
||||
Task<IEnumerable<CierreCuentaCorrienteHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idCierreAfectado);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
// Resultado del chequeo de período cerrado.
|
||||
// Si EstaCerrado=true, IdCierre y FechaCorte tienen el cierre que bloquea la operación.
|
||||
public record PeriodoCerradoResult(bool EstaCerrado, int? IdCierre, DateTime? FechaCorte);
|
||||
|
||||
public interface IPeriodoCerradoValidator
|
||||
{
|
||||
Task<PeriodoCerradoResult> EstaCerradoAsync(string destino, int idDestino, int idEmpresa, DateTime fechaOperacion);
|
||||
|
||||
// Invalida la entrada de cache para el par (Distribuidor + Empresa). Llamar después de Crear o Anular cierre.
|
||||
void InvalidarCache(int idDistribuidor, int idEmpresa);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
private readonly ICanillaRepository _canillaRepo;
|
||||
private readonly IEmpresaRepository _empresaRepo;
|
||||
private readonly ISaldoRepository _saldoRepo;
|
||||
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<NotaCreditoDebitoService> _logger;
|
||||
|
||||
@@ -30,7 +29,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
ICanillaRepository canillaRepo,
|
||||
IEmpresaRepository empresaRepo,
|
||||
ISaldoRepository saldoRepo,
|
||||
IPeriodoCerradoValidator periodoCerrado,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<NotaCreditoDebitoService> logger)
|
||||
{
|
||||
@@ -39,7 +37,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_canillaRepo = canillaRepo;
|
||||
_empresaRepo = empresaRepo;
|
||||
_saldoRepo = saldoRepo;
|
||||
_periodoCerrado = periodoCerrado;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -114,11 +111,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
|
||||
return (null, "La empresa especificada no existe.");
|
||||
|
||||
// Bloqueo por período cerrado (sólo aplica si Destino="Distribuidores"; el validator filtra internamente).
|
||||
var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync(createDto.Destino, createDto.IdDestino, createDto.IdEmpresa, createDto.Fecha);
|
||||
if (bloqueoCrear.EstaCerrado)
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);
|
||||
|
||||
var nuevaNota = new NotaCreditoDebito
|
||||
{
|
||||
Destino = createDto.Destino,
|
||||
@@ -195,14 +187,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (false, "Nota no encontrada.");
|
||||
}
|
||||
|
||||
// Bloqueo por período cerrado sobre la fecha original (el DTO de update no permite cambiar Fecha).
|
||||
var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, notaExistente.Fecha);
|
||||
if (bloqueoUpd.EstaCerrado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
|
||||
}
|
||||
|
||||
decimal impactoOriginalSaldo = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
|
||||
decimal impactoNuevoSaldo = notaExistente.Tipo == "Credito" ? -updateDto.Monto : updateDto.Monto;
|
||||
decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo;
|
||||
@@ -268,14 +252,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (false, "Nota no encontrada.");
|
||||
}
|
||||
|
||||
// Bloqueo por período cerrado: no se puede eliminar una nota cuya fecha cae en un cierre vigente.
|
||||
var bloqueoDel = await _periodoCerrado.EstaCerradoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, notaExistente.Fecha);
|
||||
if (bloqueoDel.EstaCerrado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
|
||||
}
|
||||
|
||||
decimal montoReversion = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;
|
||||
|
||||
var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction);
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
private readonly ITipoPagoRepository _tipoPagoRepo;
|
||||
private readonly IEmpresaRepository _empresaRepo;
|
||||
private readonly ISaldoRepository _saldoRepo;
|
||||
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<PagoDistribuidorService> _logger;
|
||||
|
||||
@@ -30,7 +29,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
ITipoPagoRepository tipoPagoRepo,
|
||||
IEmpresaRepository empresaRepo,
|
||||
ISaldoRepository saldoRepo,
|
||||
IPeriodoCerradoValidator periodoCerrado,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<PagoDistribuidorService> logger)
|
||||
{
|
||||
@@ -39,7 +37,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_tipoPagoRepo = tipoPagoRepo;
|
||||
_empresaRepo = empresaRepo;
|
||||
_saldoRepo = saldoRepo;
|
||||
_periodoCerrado = periodoCerrado;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -109,11 +106,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (null, mensajeError);
|
||||
}
|
||||
|
||||
// Bloqueo por período cerrado: la fecha de operación no puede caer dentro de un cierre vigente.
|
||||
var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync("Distribuidores", createDto.IdDistribuidor, createDto.IdEmpresa, createDto.Fecha);
|
||||
if (bloqueoCrear.EstaCerrado)
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);
|
||||
|
||||
var nuevoPago = new PagoDistribuidor
|
||||
{
|
||||
IdDistribuidor = createDto.IdDistribuidor,
|
||||
@@ -190,14 +182,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (false, "Pago no encontrado.");
|
||||
}
|
||||
|
||||
// Bloqueo por período cerrado sobre la fecha original del pago (el DTO de update no permite cambiar Fecha).
|
||||
var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, pagoExistente.Fecha);
|
||||
if (bloqueoUpd.EstaCerrado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
|
||||
}
|
||||
|
||||
if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
@@ -235,11 +219,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
||||
catch (BloqueoPorPeriodoCerradoException)
|
||||
{
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (Bloqueo)."); }
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor."); }
|
||||
@@ -274,14 +253,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (false, "Pago no encontrado.");
|
||||
}
|
||||
|
||||
// Bloqueo por período cerrado: no se puede eliminar un pago cuya fecha cae en un cierre vigente.
|
||||
var bloqueoDel = await _periodoCerrado.EstaCerradoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, pagoExistente.Fecha);
|
||||
if (bloqueoDel.EstaCerrado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
|
||||
}
|
||||
|
||||
decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto;
|
||||
|
||||
var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction);
|
||||
@@ -295,11 +266,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
||||
catch (BloqueoPorPeriodoCerradoException)
|
||||
{
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (Bloqueo)."); }
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor."); }
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
using GestionIntegral.Api.Data.Repositories.Contables;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
public class PeriodoCerradoValidator : IPeriodoCerradoValidator
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<PeriodoCerradoValidator> _logger;
|
||||
|
||||
// TTL del cache: ventana stale aceptada por contrato (los cierres son operación rara).
|
||||
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
|
||||
|
||||
// Singleton consumiendo Scoped (ICierreCuentaCorrienteRepository) → resolver vía IServiceScopeFactory cada lookup.
|
||||
public PeriodoCerradoValidator(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IMemoryCache cache,
|
||||
ILogger<PeriodoCerradoValidator> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PeriodoCerradoResult> EstaCerradoAsync(string destino, int idDestino, int idEmpresa, DateTime fechaOperacion)
|
||||
{
|
||||
// Sólo aplica a Distribuidores. Canillas no tiene cierre por (Canilla + Empresa).
|
||||
if (!string.Equals(destino, "Distribuidores", StringComparison.OrdinalIgnoreCase))
|
||||
return new PeriodoCerradoResult(false, null, null);
|
||||
|
||||
var key = CacheKey(idDestino, idEmpresa);
|
||||
|
||||
var ultimo = await _cache.GetOrCreateAsync(key, async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = CacheTtl;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICierreCuentaCorrienteRepository>();
|
||||
var cierre = await repo.GetUltimoCierreVigenteAsync(idDestino, idEmpresa);
|
||||
|
||||
// Cacheamos sólo lo necesario para que la decisión sea rápida.
|
||||
return cierre == null ? null : new CachedUltimoCierre(cierre.IdCierre, cierre.FechaCorte);
|
||||
});
|
||||
|
||||
if (ultimo == null)
|
||||
return new PeriodoCerradoResult(false, null, null);
|
||||
|
||||
// FechaCorte es DATE; comparar en .Date para ignorar componente hora.
|
||||
if (ultimo.FechaCorte.Date >= fechaOperacion.Date)
|
||||
return new PeriodoCerradoResult(true, ultimo.IdCierre, ultimo.FechaCorte);
|
||||
|
||||
return new PeriodoCerradoResult(false, null, null);
|
||||
}
|
||||
|
||||
public void InvalidarCache(int idDistribuidor, int idEmpresa)
|
||||
{
|
||||
_cache.Remove(CacheKey(idDistribuidor, idEmpresa));
|
||||
}
|
||||
|
||||
private static string CacheKey(int idDestino, int idEmpresa) => $"cierre-ultimo:{idDestino}:{idEmpresa}";
|
||||
|
||||
private sealed record CachedUltimoCierre(int IdCierre, DateTime FechaCorte);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
private readonly IDistribuidorRepository _distribuidorRepo; // Para nombres
|
||||
private readonly ICanillaRepository _canillaRepo; // Para nombres
|
||||
private readonly IEmpresaRepository _empresaRepo; // Para nombres
|
||||
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<SaldoService> _logger;
|
||||
|
||||
@@ -28,7 +27,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
IDistribuidorRepository distribuidorRepo,
|
||||
ICanillaRepository canillaRepo,
|
||||
IEmpresaRepository empresaRepo,
|
||||
IPeriodoCerradoValidator periodoCerrado,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<SaldoService> logger)
|
||||
{
|
||||
@@ -36,7 +34,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_distribuidorRepo = distribuidorRepo;
|
||||
_canillaRepo = canillaRepo;
|
||||
_empresaRepo = empresaRepo;
|
||||
_periodoCerrado = periodoCerrado;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -106,11 +103,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
if (await _empresaRepo.GetByIdAsync(ajusteDto.IdEmpresa) == null)
|
||||
return (false, "La empresa especificada no existe.", null);
|
||||
|
||||
// Bloqueo por período cerrado: la FechaOperacion del ajuste no puede caer dentro de un cierre vigente.
|
||||
// El validator filtra internamente Destino="Distribuidores"; ajustes a Canillas no se bloquean.
|
||||
var bloqueo = await _periodoCerrado.EstaCerradoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, ajusteDto.FechaOperacion);
|
||||
if (bloqueo.EstaCerrado)
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueo.IdCierre!.Value, bloqueo.FechaCorte!.Value);
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection.State != ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); }
|
||||
@@ -160,11 +152,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
var saldoDtoActualizado = await MapToGestionDto(saldoDespuesDeModificacion);
|
||||
return (true, null, saldoDtoActualizado);
|
||||
}
|
||||
catch (BloqueoPorPeriodoCerradoException)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync (Bloqueo)."); }
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync."); }
|
||||
|
||||
@@ -56,22 +56,20 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
Depto = data.Distribuidor.Depto,
|
||||
Telefono = data.Distribuidor.Telefono,
|
||||
Email = data.Distribuidor.Email,
|
||||
Localidad = data.Distribuidor.Localidad,
|
||||
Baja = data.Distribuidor.Baja,
|
||||
FechaBaja = data.Distribuidor.FechaBaja
|
||||
Localidad = data.Distribuidor.Localidad
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true)
|
||||
public async Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter)
|
||||
{
|
||||
var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter, soloActivos);
|
||||
var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter);
|
||||
// Filtrar nulos y asegurar al compilador que no hay nulos en la lista final
|
||||
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync(bool? soloActivos = true)
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync()
|
||||
{
|
||||
var data = await _distribuidorRepository.GetAllDropdownAsync(soloActivos);
|
||||
var data = await _distribuidorRepository.GetAllDropdownAsync();
|
||||
// Asegurar que el resultado no sea nulo y no contiene elementos nulos
|
||||
if (data == null)
|
||||
{
|
||||
@@ -225,31 +223,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario)
|
||||
{
|
||||
var distribuidorExistente = await _distribuidorRepository.GetByIdSimpleAsync(id);
|
||||
if (distribuidorExistente == null) return (false, "Distribuidor no encontrado.");
|
||||
|
||||
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 toggled = await _distribuidorRepository.ToggleBajaAsync(id, darDeBaja, fechaBaja, idUsuario, transaction);
|
||||
if (!toggled) throw new DataException("Error al cambiar estado de baja.");
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Distribuidor ID {IdDistribuidor} dado de {Estado} por Usuario ID {IdUsuario}.", id, darDeBaja ? "baja" : "alta", idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Distribuidor no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ToggleBajaAsync Distribuidor ID: {IdDistribuidor}", id);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -4,7 +4,6 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Dtos.Auditoria;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using GestionIntegral.Api.Services.Contables;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -25,7 +24,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
private readonly IPorcPagoRepository _porcPagoRepository;
|
||||
private readonly ISaldoRepository _saldoRepository;
|
||||
private readonly IEmpresaRepository _empresaRepository; // Para obtener IdEmpresa de la publicación
|
||||
private readonly IPeriodoCerradoValidator _periodoCerrado;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<EntradaSalidaDistService> _logger;
|
||||
|
||||
@@ -38,7 +36,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
IPorcPagoRepository porcPagoRepository,
|
||||
ISaldoRepository saldoRepository,
|
||||
IEmpresaRepository empresaRepository,
|
||||
IPeriodoCerradoValidator periodoCerrado,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<EntradaSalidaDistService> logger)
|
||||
{
|
||||
@@ -50,7 +47,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
_porcPagoRepository = porcPagoRepository;
|
||||
_saldoRepository = saldoRepository;
|
||||
_empresaRepository = empresaRepository;
|
||||
_periodoCerrado = periodoCerrado;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -171,16 +167,10 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor);
|
||||
if (distribuidor == null) return (null, "Distribuidor no válido.");
|
||||
|
||||
// Bloqueo por período cerrado: la fecha del movimiento no puede caer dentro de un cierre vigente del distribuidor en la empresa de la publicación.
|
||||
var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync("Distribuidores", createDto.IdDistribuidor, publicacion.IdEmpresa, createDto.Fecha);
|
||||
if (bloqueoCrear.EstaCerrado)
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);
|
||||
|
||||
/*
|
||||
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);
|
||||
@@ -271,14 +261,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
|
||||
if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");
|
||||
|
||||
// Bloqueo por período cerrado sobre la fecha original del movimiento (el DTO de update no permite cambiar Fecha).
|
||||
var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, esExistente.Fecha);
|
||||
if (bloqueoUpd.EstaCerrado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
|
||||
}
|
||||
|
||||
|
||||
// 1. Calcular monto del movimiento original (antes de la actualización)
|
||||
decimal montoOriginal = await CalcularMontoMovimiento(
|
||||
@@ -324,11 +306,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
||||
catch (BloqueoPorPeriodoCerradoException)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
@@ -352,14 +329,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
|
||||
if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");
|
||||
|
||||
// Bloqueo por período cerrado: no se puede eliminar un movimiento cuya fecha cae en un cierre vigente.
|
||||
var bloqueoDel = await _periodoCerrado.EstaCerradoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, esExistente.Fecha);
|
||||
if (bloqueoDel.EstaCerrado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -394,11 +363,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
|
||||
catch (BloqueoPorPeriodoCerradoException)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
|
||||
@@ -7,13 +7,12 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public interface IDistribuidorService
|
||||
{
|
||||
Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true);
|
||||
Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter);
|
||||
Task<DistribuidorDto?> ObtenerPorIdAsync(int id);
|
||||
Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario);
|
||||
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync(bool? soloActivos = true);
|
||||
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync();
|
||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
{
|
||||
Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta);
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta);
|
||||
|
||||
Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina);
|
||||
Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario);
|
||||
@@ -21,8 +21,5 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro);
|
||||
Task<IEnumerable<StockBobinaDto>> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito);
|
||||
Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -85,9 +85,9 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
|
||||
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta)
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
||||
{
|
||||
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta, fechaEstadoDesde, fechaEstadoHasta);
|
||||
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta);
|
||||
var dtos = new List<StockBobinaDto>();
|
||||
foreach (var bobina in bobinas)
|
||||
{
|
||||
@@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
}
|
||||
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.");
|
||||
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;
|
||||
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");
|
||||
@@ -199,20 +199,12 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
|
||||
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción
|
||||
if (bobina == null)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
return (false, "Bobina no encontrada.");
|
||||
}
|
||||
|
||||
// Comparamos solo las fechas (sin hora) para evitar problemas de precisión.
|
||||
if (cambiarEstadoDto.FechaCambioEstado.Date < bobina.FechaRemito.Date)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
return (false, $"Error de integridad: La fecha del nuevo estado ({cambiarEstadoDto.FechaCambioEstado:dd/MM/yyyy}) " +
|
||||
$"no puede ser anterior a la fecha de ingreso por remito ({bobina.FechaRemito:dd/MM/yyyy}).");
|
||||
}
|
||||
|
||||
var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
|
||||
if (nuevoEstado == null)
|
||||
@@ -391,153 +383,5 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
TipoMod = h.Historial.TipoMod
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<StockBobinaDto>> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito)
|
||||
{
|
||||
// Si la fecha tiene valor, filtramos por ese día exacto. Si no, busca en cualquier fecha.
|
||||
DateTime? fechaDesde = fechaRemito?.Date;
|
||||
DateTime? fechaHasta = fechaRemito?.Date;
|
||||
|
||||
var bobinas = await _stockBobinaRepository.GetAllAsync(null, null, idPlanta, null, remito, fechaDesde, fechaHasta, null, null);
|
||||
|
||||
var dtos = new List<StockBobinaDto>();
|
||||
foreach (var bobina in bobinas)
|
||||
{
|
||||
dtos.Add(await MapToDto(bobina));
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario)
|
||||
{
|
||||
// 1. Buscar todas las bobinas que coinciden con el lote a modificar.
|
||||
var bobinasAActualizar = await _stockBobinaRepository.GetAllAsync(
|
||||
idTipoBobina: null,
|
||||
nroBobinaFilter: null,
|
||||
idPlanta: dto.IdPlanta,
|
||||
idEstadoBobina: null,
|
||||
remitoFilter: dto.Remito,
|
||||
fechaDesde: dto.FechaRemitoActual.Date,
|
||||
fechaHasta: dto.FechaRemitoActual.Date,
|
||||
fechaEstadoDesde: null,
|
||||
fechaEstadoHasta: null
|
||||
);
|
||||
|
||||
if (!bobinasAActualizar.Any())
|
||||
{
|
||||
return (false, "No se encontraron bobinas para el remito, planta y fecha especificados. Es posible que ya hayan sido modificados.");
|
||||
}
|
||||
|
||||
// 2. Iniciar una transacción para asegurar que todas las actualizaciones se completen o ninguna.
|
||||
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
|
||||
{
|
||||
// 3. Iterar sobre cada bobina y actualizarla.
|
||||
foreach (var bobina in bobinasAActualizar)
|
||||
{
|
||||
// Modificamos solo la fecha del remito.
|
||||
bobina.FechaRemito = dto.NuevaFechaRemito.Date;
|
||||
|
||||
// Reutilizamos el método UpdateAsync que ya maneja la lógica de historial.
|
||||
// Le pasamos un mensaje específico para el historial.
|
||||
await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, "Fecha Remito Corregida");
|
||||
}
|
||||
|
||||
// 4. Si todo salió bien, confirmar la transacción.
|
||||
transaction.Commit();
|
||||
_logger.LogInformation(
|
||||
"{Count} bobinas del remito {Remito} (Planta ID {IdPlanta}) actualizadas a nueva fecha {NuevaFecha} por Usuario ID {IdUsuario}.",
|
||||
bobinasAActualizar.Count(), dto.Remito, dto.IdPlanta, dto.NuevaFechaRemito.Date, idUsuario
|
||||
);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 5. Si algo falla, revertir todos los cambios.
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error transaccional al actualizar fecha de remito {Remito}.", dto.Remito);
|
||||
return (false, $"Error interno al actualizar el lote: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario)
|
||||
{
|
||||
// --- FASE 1: VALIDACIÓN PREVIA (FUERA DE LA TRANSACCIÓN) ---
|
||||
|
||||
// Validación de la cabecera
|
||||
if (await _plantaRepository.GetByIdAsync(loteDto.IdPlanta) == null)
|
||||
return (false, "La planta especificada no es válida.");
|
||||
|
||||
// Validación de cada bobina del lote
|
||||
foreach (var bobinaDetalle in loteDto.Bobinas)
|
||||
{
|
||||
if (await _tipoBobinaRepository.GetByIdAsync(bobinaDetalle.IdTipoBobina) == null)
|
||||
{
|
||||
return (false, $"El tipo de bobina con ID {bobinaDetalle.IdTipoBobina} no es válido.");
|
||||
}
|
||||
|
||||
// Esta es la lectura que causaba el bloqueo. Ahora se hace ANTES de la transacción.
|
||||
if (await _stockBobinaRepository.GetByNroBobinaAsync(bobinaDetalle.NroBobina) != null)
|
||||
{
|
||||
return (false, $"El número de bobina '{bobinaDetalle.NroBobina}' ya existe en el sistema.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validación de números de bobina duplicados dentro del mismo lote
|
||||
var nrosBobinaEnLote = loteDto.Bobinas.Select(b => b.NroBobina.Trim()).ToList();
|
||||
if (nrosBobinaEnLote.Count != nrosBobinaEnLote.Distinct().Count())
|
||||
{
|
||||
var duplicado = nrosBobinaEnLote.GroupBy(n => n).Where(g => g.Count() > 1).First().Key;
|
||||
return (false, $"El número de bobina '{duplicado}' está duplicado en el lote que intenta ingresar.");
|
||||
}
|
||||
|
||||
|
||||
// --- FASE 2: ESCRITURA TRANSACCIONAL ---
|
||||
|
||||
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
|
||||
{
|
||||
// Ahora este bucle solo contiene operaciones de escritura. No habrá bloqueos.
|
||||
foreach (var bobinaDetalle in loteDto.Bobinas)
|
||||
{
|
||||
var nuevaBobina = new StockBobina
|
||||
{
|
||||
IdTipoBobina = bobinaDetalle.IdTipoBobina,
|
||||
NroBobina = bobinaDetalle.NroBobina,
|
||||
Peso = bobinaDetalle.Peso,
|
||||
IdPlanta = loteDto.IdPlanta,
|
||||
Remito = loteDto.Remito,
|
||||
FechaRemito = loteDto.FechaRemito.Date,
|
||||
IdEstadoBobina = 1, // 1 = Disponible
|
||||
FechaEstado = loteDto.FechaRemito.Date,
|
||||
IdPublicacion = null,
|
||||
IdSeccion = null,
|
||||
Obs = null
|
||||
};
|
||||
|
||||
var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction);
|
||||
if (bobinaCreada == null)
|
||||
{
|
||||
throw new DataException($"No se pudo crear el registro para la bobina '{nuevaBobina.NroBobina}'.");
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Lote de {Count} bobinas para remito {Remito} ingresado por Usuario ID {UserId}.", loteDto.Bobinas.Count, loteDto.Remito, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al ingresar lote de bobinas para remito {Remito}", loteDto.Remito);
|
||||
return (false, $"Error interno al procesar el lote: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,14 +53,12 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
Task<(IEnumerable<ComparativaConsumoBobinasDto> Data, string? Error)> ObtenerComparativaConsumoBobinasAsync(DateTime fechaInicioMesA, DateTime fechaFinMesA, DateTime fechaInicioMesB, DateTime fechaFinMesB, int idPlanta);
|
||||
Task<(IEnumerable<ComparativaConsumoBobinasDto> Data, string? Error)> ObtenerComparativaConsumoBobinasConsolidadoAsync(DateTime fechaInicioMesA, DateTime fechaFinMesA, DateTime fechaInicioMesB, DateTime fechaFinMesB);
|
||||
|
||||
// DTOs para ReporteCuentasDistribuidores.
|
||||
// El antiguo IEnumerable<SaldoDto> Saldos (saldo actual del distribuidor, sin sentido temporal en un reporte por período)
|
||||
// se reemplazó por decimal SaldoInicial calculado en base al último cierre vigente.
|
||||
// DTOs para ReporteCuentasDistribuidores
|
||||
Task<(
|
||||
IEnumerable<BalanceCuentaDistDto> EntradasSalidas,
|
||||
IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos,
|
||||
IEnumerable<BalanceCuentaPagosDto> Pagos,
|
||||
decimal SaldoInicial,
|
||||
IEnumerable<SaldoDto> Saldos,
|
||||
string? Error
|
||||
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
|
||||
@@ -2,7 +2,6 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Data.Repositories.Reportes;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Dtos.Reportes;
|
||||
using GestionIntegral.Api.Services.Contables;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Reportes
|
||||
{
|
||||
@@ -15,11 +14,10 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
private readonly IEmpresaRepository _empresaRepository;
|
||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||
private readonly ISuscripcionRepository _suscripcionRepository;
|
||||
private readonly ICierreCuentaCorrienteService _cierreService;
|
||||
private readonly ILogger<ReportesService> _logger;
|
||||
|
||||
public ReportesService(IReportesRepository reportesRepository, IFacturaRepository facturaRepository, IFacturaDetalleRepository facturaDetalleRepository, IPublicacionRepository publicacionRepository, IEmpresaRepository empresaRepository
|
||||
, ISuscriptorRepository suscriptorRepository, ISuscripcionRepository suscripcionRepository, ICierreCuentaCorrienteService cierreService, ILogger<ReportesService> logger)
|
||||
, ISuscriptorRepository suscriptorRepository, ISuscripcionRepository suscripcionRepository, ILogger<ReportesService> logger)
|
||||
{
|
||||
_reportesRepository = reportesRepository;
|
||||
_facturaRepository = facturaRepository;
|
||||
@@ -28,7 +26,6 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
_empresaRepository = empresaRepository;
|
||||
_suscriptorRepository = suscriptorRepository;
|
||||
_suscripcionRepository = suscripcionRepository;
|
||||
_cierreService = cierreService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -423,28 +420,26 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
}
|
||||
}
|
||||
|
||||
// Implementación para ReporteCuentasDistribuidores.
|
||||
// Reemplaza el campo legacy "Saldos" (saldo actual sin sentido temporal) por "SaldoInicial":
|
||||
// saldo del último cierre + movimientos netos hasta fechaDesde-1 (0 si no hay cierre previo).
|
||||
// Implementación para ReporteCuentasDistribuidores
|
||||
public async Task<(
|
||||
IEnumerable<BalanceCuentaDistDto> EntradasSalidas,
|
||||
IEnumerable<BalanceCuentaDebCredDto> DebitosCreditos,
|
||||
IEnumerable<BalanceCuentaPagosDto> Pagos,
|
||||
decimal SaldoInicial,
|
||||
IEnumerable<SaldoDto> Saldos,
|
||||
string? Error
|
||||
)> ObtenerReporteCuentasDistribuidorAsync(int idDistribuidor, int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
if (fechaDesde > fechaHasta)
|
||||
return (Enumerable.Empty<BalanceCuentaDistDto>(), Enumerable.Empty<BalanceCuentaDebCredDto>(), Enumerable.Empty<BalanceCuentaPagosDto>(), 0m, "Fecha 'Desde' no puede ser mayor que 'Hasta'.");
|
||||
return (Enumerable.Empty<BalanceCuentaDistDto>(), Enumerable.Empty<BalanceCuentaDebCredDto>(), Enumerable.Empty<BalanceCuentaPagosDto>(), Enumerable.Empty<SaldoDto>(), "Fecha 'Desde' no puede ser mayor que 'Hasta'.");
|
||||
|
||||
try
|
||||
{
|
||||
var esTask = _reportesRepository.GetBalanceCuentaDistEntradaSalidaPorEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||
var dcTask = _reportesRepository.GetBalanceCuentDistDebCredEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||
var paTask = _reportesRepository.GetBalanceCuentDistPagosEmpresaAsync(idDistribuidor, idEmpresa, fechaDesde, fechaHasta);
|
||||
var siTask = _cierreService.CalcularSaldoInicialReporteAsync(idDistribuidor, idEmpresa, fechaDesde);
|
||||
var saTask = _reportesRepository.GetBalanceCuentSaldosEmpresasAsync("Distribuidores", idDistribuidor, idEmpresa);
|
||||
|
||||
await Task.WhenAll(esTask, dcTask, paTask, siTask);
|
||||
await Task.WhenAll(esTask, dcTask, paTask, saTask);
|
||||
|
||||
Func<IEnumerable<BalanceCuentaDistDto>, IEnumerable<BalanceCuentaDistDto>> esToUtc =
|
||||
items => items?.Select(i => { i.Fecha = DateTime.SpecifyKind(i.Fecha.Date, DateTimeKind.Utc); return i; }).ToList()
|
||||
@@ -460,7 +455,7 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
esToUtc(await esTask),
|
||||
dcToUtc(await dcTask),
|
||||
paToUtc(await paTask),
|
||||
await siTask,
|
||||
await saTask ?? Enumerable.Empty<SaldoDto>(),
|
||||
null
|
||||
);
|
||||
}
|
||||
@@ -471,7 +466,7 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
Enumerable.Empty<BalanceCuentaDistDto>(),
|
||||
Enumerable.Empty<BalanceCuentaDebCredDto>(),
|
||||
Enumerable.Empty<BalanceCuentaPagosDto>(),
|
||||
0m,
|
||||
Enumerable.Empty<SaldoDto>(),
|
||||
"Error interno al generar el reporte."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
}) => {
|
||||
const [montoAjuste, setMontoAjuste] = useState<string>('');
|
||||
const [justificacion, setJustificacion] = useState('');
|
||||
const [fechaOperacion, setFechaOperacion] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -44,10 +43,10 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
if (open) {
|
||||
setMontoAjuste('');
|
||||
setJustificacion('');
|
||||
setFechaOperacion(new Date().toISOString().split('T')[0]);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
@@ -66,16 +65,11 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
} else if (justificacion.trim().length < 5 || justificacion.trim().length > 250) {
|
||||
errors.justificacion = 'La justificación debe tener entre 5 y 250 caracteres.';
|
||||
}
|
||||
|
||||
if (!fechaOperacion) {
|
||||
errors.fechaOperacion = 'La fecha de operación es obligatoria.';
|
||||
}
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion' | 'fechaOperacion') => {
|
||||
const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion') => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
@@ -93,7 +87,6 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
idEmpresa: saldoParaAjustar.idEmpresa,
|
||||
montoAjuste: parseFloat(montoAjuste),
|
||||
justificacion,
|
||||
fechaOperacion,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
onClose(); // Cerrar en éxito (el padre recargará)
|
||||
@@ -124,19 +117,6 @@ const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
</Typography>
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label="Fecha de Operación"
|
||||
type="date"
|
||||
fullWidth
|
||||
required
|
||||
value={fechaOperacion}
|
||||
onChange={(e) => { setFechaOperacion(e.target.value); handleInputChange('fechaOperacion'); }}
|
||||
margin="normal"
|
||||
error={!!localErrors.fechaOperacion}
|
||||
helperText={localErrors.fechaOperacion || ''}
|
||||
disabled={loading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Monto de Ajuste (+/-)"
|
||||
type="number"
|
||||
|
||||
@@ -114,8 +114,9 @@ const NotaCreditoDebitoFormModal: React.FC<NotaCreditoDebitoFormModalProps> = ({
|
||||
setObservaciones(initialData?.observaciones || '');
|
||||
setIdEmpresa(initialData?.idEmpresa || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, fetchEmpresas, fetchDestinatarios]);
|
||||
}, [open, initialData, clearErrorMessage, fetchEmpresas, fetchDestinatarios]);
|
||||
|
||||
useEffect(() => {
|
||||
if(open && !isEditing) { // Solo cambiar destinatarios si es creación y cambia el tipo de Destino
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { CrearCierreDto } from '../../../models/dtos/Contables/CrearCierreDto';
|
||||
import type { CierreCuentaCorrienteDto } from '../../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||
import type { DistribuidorDropdownDto } from '../../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import type { EmpresaDropdownDto } from '../../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import distribuidorService from '../../../services/Distribucion/distribuidorService';
|
||||
import empresaService from '../../../services/Distribucion/empresaService';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as const,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 600 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface NuevoCierreModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CrearCierreDto) => Promise<CierreCuentaCorrienteDto>;
|
||||
initialIdDistribuidor?: number | null;
|
||||
initialIdEmpresa?: number | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const todayIso = () => new Date().toISOString().split('T')[0];
|
||||
|
||||
const NuevoCierreModal: React.FC<NuevoCierreModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialIdDistribuidor,
|
||||
initialIdEmpresa,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idDistribuidor, setIdDistribuidor] = useState<number | string>('');
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [fechaCorte, setFechaCorte] = useState<string>(todayIso());
|
||||
const [justificacion, setJustificacion] = useState<string>('');
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
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 [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidoresDropdown(),
|
||||
empresaService.getEmpresasDropdown()
|
||||
]);
|
||||
setDistribuidores(distData);
|
||||
setEmpresas(empData);
|
||||
} catch (err) {
|
||||
console.error('Error al cargar dropdowns', err);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar distribuidores o empresas.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
setIdDistribuidor(initialIdDistribuidor ?? '');
|
||||
setIdEmpresa(initialIdEmpresa ?? '');
|
||||
setFechaCorte(todayIso());
|
||||
setJustificacion('');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialIdDistribuidor, initialIdEmpresa, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.';
|
||||
if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa.';
|
||||
if (!fechaCorte) {
|
||||
errors.fechaCorte = 'La fecha de corte es obligatoria.';
|
||||
} else if (new Date(fechaCorte) > new Date(todayIso())) {
|
||||
errors.fechaCorte = 'La fecha de corte no puede ser futura.';
|
||||
}
|
||||
if (justificacion.length > 500) {
|
||||
errors.justificacion = 'La justificación no puede superar los 500 caracteres.';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const distSeleccionado = distribuidores.find(d => d.idDistribuidor === Number(idDistribuidor));
|
||||
const empSeleccionada = empresas.find(e => e.idEmpresa === Number(idEmpresa));
|
||||
const confirmMsg = `Estás por cerrar el período hasta ${fechaCorte} para "${distSeleccionado?.nombre ?? ''}" en "${empSeleccionada?.nombre ?? ''}".\n\n` +
|
||||
`Después de cerrar no se podrán registrar movimientos, pagos, notas ni ajustes con fecha menor o igual a esta. ¿Continuar?`;
|
||||
if (!window.confirm(confirmMsg)) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dto: CrearCierreDto = {
|
||||
idDistribuidor: Number(idDistribuidor),
|
||||
idEmpresa: Number(idEmpresa),
|
||||
fechaCorte,
|
||||
justificacion: justificacion.trim() || null
|
||||
};
|
||||
await onSubmit(dto);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('Error al crear cierre:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Nuevo Cierre de Cuenta Corriente
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idDistribuidor} required>
|
||||
<InputLabel id="distribuidor-cierre-label">Distribuidor</InputLabel>
|
||||
<Select
|
||||
labelId="distribuidor-cierre-label"
|
||||
label="Distribuidor"
|
||||
value={idDistribuidor}
|
||||
onChange={(e) => { setIdDistribuidor(e.target.value as number); handleInputChange('idDistribuidor'); }}
|
||||
disabled={loading || loadingDropdowns}
|
||||
>
|
||||
<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>
|
||||
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idEmpresa} required>
|
||||
<InputLabel id="empresa-cierre-label">Empresa</InputLabel>
|
||||
<Select
|
||||
labelId="empresa-cierre-label"
|
||||
label="Empresa"
|
||||
value={idEmpresa}
|
||||
onChange={(e) => { setIdEmpresa(e.target.value as number); handleInputChange('idEmpresa'); }}
|
||||
disabled={loading || loadingDropdowns}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione</em></MenuItem>
|
||||
{empresas.map((e) => (
|
||||
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption">{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Fecha de Corte"
|
||||
type="date"
|
||||
value={fechaCorte}
|
||||
required
|
||||
onChange={(e) => { setFechaCorte(e.target.value); handleInputChange('fechaCorte'); }}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
error={!!localErrors.fechaCorte}
|
||||
helperText={localErrors.fechaCorte || 'No puede ser una fecha futura.'}
|
||||
disabled={loading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ max: todayIso() }}
|
||||
/>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 0.5 }}>
|
||||
La fecha de corte es inclusive: los movimientos con fecha igual a la seleccionada se
|
||||
incluyen en el cálculo del saldo de cierre.
|
||||
</Alert>
|
||||
|
||||
<TextField
|
||||
label="Justificación (opcional)"
|
||||
value={justificacion}
|
||||
onChange={(e) => { setJustificacion(e.target.value); handleInputChange('justificacion'); }}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
disabled={loading}
|
||||
error={!!localErrors.justificacion}
|
||||
helperText={localErrors.justificacion || `${justificacion.length}/500 caracteres`}
|
||||
inputProps={{ maxLength: 500 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
Una vez creado el cierre, no podrán registrarse, modificarse ni eliminarse pagos, notas, ajustes
|
||||
o movimientos cuya fecha de operación sea menor o igual a la fecha de corte.
|
||||
</Alert>
|
||||
|
||||
{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} /> : 'Crear Cierre'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NuevoCierreModal;
|
||||
@@ -95,8 +95,9 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
||||
setDetalle(initialData?.detalle || '');
|
||||
setIdEmpresa(initialData?.idEmpresa || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData]);
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import type { ReabrirCierreDto } from '../../../models/dtos/Contables/ReabrirCierreDto';
|
||||
import type { CierreCuentaCorrienteDto } from '../../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as const,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 560 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
const MIN_LEN = 10;
|
||||
const MAX_LEN = 500;
|
||||
|
||||
interface ReabrirCierreModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (idCierre: number, data: ReabrirCierreDto) => Promise<CierreCuentaCorrienteDto>;
|
||||
cierre: CierreCuentaCorrienteDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const ReabrirCierreModal: React.FC<ReabrirCierreModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
cierre,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [justificacion, setJustificacion] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setJustificacion('');
|
||||
setLocalError(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, clearErrorMessage]);
|
||||
|
||||
const justifTrimLen = justificacion.trim().length;
|
||||
const submitDisabled = loading || justifTrimLen < MIN_LEN || justifTrimLen > MAX_LEN;
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!cierre) return;
|
||||
if (justifTrimLen < MIN_LEN) {
|
||||
setLocalError(`La justificación debe tener al menos ${MIN_LEN} caracteres.`);
|
||||
return;
|
||||
}
|
||||
if (justifTrimLen > MAX_LEN) {
|
||||
setLocalError(`La justificación no puede superar ${MAX_LEN} caracteres.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalError(null);
|
||||
clearErrorMessage();
|
||||
setLoading(true);
|
||||
try {
|
||||
await onSubmit(cierre.idCierre, { justificacion: justificacion.trim() });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('Error al reabrir cierre:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Reabrir Cierre de Cuenta Corriente
|
||||
</Typography>
|
||||
|
||||
<Alert severity="warning" icon={<WarningAmberIcon />} sx={{ mb: 2 }}>
|
||||
<strong>ATENCIÓN:</strong> estás por reabrir un cierre. Esta acción quedará registrada en auditoría.
|
||||
Solo se puede reabrir el último cierre vigente. Si hay cierres posteriores, primero hay que reabrirlos a ellos.
|
||||
</Alert>
|
||||
|
||||
{cierre && (
|
||||
<Box sx={{ mb: 2, p: 1.5, bgcolor: 'grey.100', borderRadius: 1 }}>
|
||||
<Typography variant="body2"><strong>Distribuidor:</strong> {cierre.nombreDistribuidor}</Typography>
|
||||
<Typography variant="body2"><strong>Empresa:</strong> {cierre.nombreEmpresa}</Typography>
|
||||
<Typography variant="body2"><strong>Fecha de Corte:</strong> {cierre.fechaCorte}</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Saldo del Cierre:</strong>{' '}
|
||||
{cierre.saldoCierre.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
label="Justificación de la reapertura"
|
||||
value={justificacion}
|
||||
onChange={(e) => { setJustificacion(e.target.value); if (localError) setLocalError(null); }}
|
||||
required
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
error={!!localError}
|
||||
helperText={
|
||||
localError ||
|
||||
`Mínimo ${MIN_LEN} caracteres — ${justifTrimLen}/${MAX_LEN}`
|
||||
}
|
||||
inputProps={{ maxLength: MAX_LEN }}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{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" color="warning" disabled={submitDisabled}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Confirmar Reapertura'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReabrirCierreModal;
|
||||
@@ -86,8 +86,9 @@ const EntradaSalidaDistFormModal: React.FC<EntradaSalidaDistFormModalProps> = ({
|
||||
setRemito(initialData?.remito?.toString() || '');
|
||||
setObservacion(initialData?.observacion || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData]);
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
// --- CAMBIO: Importar PublicacionDropdownDto ---
|
||||
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
|
||||
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService';
|
||||
@@ -32,288 +33,272 @@ const ID_ESTADO_EN_USO = 2; // Usaremos este consistentemente
|
||||
const ID_ESTADO_DANADA = 3;
|
||||
|
||||
interface StockBobinaCambioEstadoModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
||||
bobinaActual: StockBobinaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
||||
bobinaActual: StockBobinaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
bobinaActual,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
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 [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<PublicacionDropdownDto[]>([]);
|
||||
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
||||
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
|
||||
// --- CAMBIO: Usar PublicacionDropdownDto para el estado ---
|
||||
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
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 todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
|
||||
let estadosFiltrados: EstadoBobinaDto[];
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
if (!bobinaActual) return;
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
|
||||
let estadosFiltrados: EstadoBobinaDto[];
|
||||
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) {
|
||||
estadosFiltrados = todosLosEstados.filter(
|
||||
e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA
|
||||
);
|
||||
} else {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina);
|
||||
}
|
||||
|
||||
setEstadosDisponibles(estadosFiltrados);
|
||||
|
||||
const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO);
|
||||
|
||||
if (sePuedePonerEnUso) {
|
||||
const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicacionesDisponibles(publicacionesData);
|
||||
} else {
|
||||
setPublicacionesDisponibles([]);
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
|
||||
} 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('');
|
||||
// Pre-cargar basado en si la bobina actual está "En Uso"
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion(bobinaActual.idPublicacion?.toString() || '');
|
||||
// Solo pre-cargar sección si la publicación también estaba pre-cargada
|
||||
if (bobinaActual.idPublicacion) {
|
||||
setIdSeccion(bobinaActual.idSeccion?.toString() || '');
|
||||
} else {
|
||||
setIdSeccion('');
|
||||
}
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) {
|
||||
// --- CAMBIO: Usar ID_ESTADO_EN_USO ---
|
||||
estadosFiltrados = todosLosEstados.filter(
|
||||
e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA
|
||||
);
|
||||
} else {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina);
|
||||
}
|
||||
|
||||
setEstadosDisponibles(estadosFiltrados);
|
||||
|
||||
const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO);
|
||||
|
||||
if (sePuedePonerEnUso) {
|
||||
// --- CAMBIO: La data es PublicacionDropdownDto[] ---
|
||||
const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicacionesDisponibles(publicacionesData);
|
||||
} else {
|
||||
setPublicacionesDisponibles([]);
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
setObs(bobinaActual.obs || '');
|
||||
setFechaCambioEstado(new Date().toISOString().split('T')[0]);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
|
||||
} 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);
|
||||
}
|
||||
}, [open, bobinaActual, clearErrorMessage]);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSecciones = async () => {
|
||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true);
|
||||
setSeccionesDisponibles(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar secciones:", error);
|
||||
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.' }));
|
||||
setSeccionesDisponibles([]);
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
if (open && bobinaActual) {
|
||||
fetchDropdownData();
|
||||
setNuevoEstadoId('');
|
||||
// Pre-cargar basado en si la bobina actual está "En Uso"
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion(bobinaActual.idPublicacion?.toString() || '');
|
||||
// Solo pre-cargar sección si la publicación también estaba pre-cargada
|
||||
if (bobinaActual.idPublicacion) {
|
||||
setIdSeccion(bobinaActual.idSeccion?.toString() || '');
|
||||
} else {
|
||||
setSeccionesDisponibles([]);
|
||||
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace.
|
||||
setIdSeccion('');
|
||||
}
|
||||
};
|
||||
if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion
|
||||
fetchSecciones();
|
||||
} else {
|
||||
setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
|
||||
}
|
||||
}, [nuevoEstadoId, idPublicacion]);
|
||||
|
||||
|
||||
// Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso"
|
||||
useEffect(() => {
|
||||
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
}, [nuevoEstadoId]);
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
||||
if (!fechaCambioEstado.trim()) {
|
||||
errors.fechaCambioEstado = 'La fecha es obligatoria.';
|
||||
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) {
|
||||
errors.fechaCambioEstado = 'Formato de fecha inválido.';
|
||||
} else if (bobinaActual) {
|
||||
const fechaRemitoSimple = bobinaActual.fechaRemito.split('T')[0];
|
||||
if (fechaCambioEstado < fechaRemitoSimple) {
|
||||
errors.fechaCambioEstado = `La fecha no puede ser anterior al ingreso (${fechaRemitoSimple}).`;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId
|
||||
// y el de sección a un useEffect de idPublicacion
|
||||
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
|
||||
setIdSeccion('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setObs(bobinaActual.obs || '');
|
||||
setFechaCambioEstado(new Date().toISOString().split('T')[0]);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
if (!validate() || !bobinaActual) return;
|
||||
}
|
||||
}, [open, bobinaActual, clearErrorMessage]);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
|
||||
const dataToSubmit: CambiarEstadoBobinaDto = {
|
||||
nuevoEstadoId: Number(nuevoEstadoId),
|
||||
idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
|
||||
idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null,
|
||||
obs: obs.trim() || null,
|
||||
fechaCambioEstado,
|
||||
};
|
||||
await onSubmit(bobinaActual.idBobina, dataToSubmit);
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de StockBobinaCambioEstadoModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSecciones = async () => {
|
||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true);
|
||||
setSeccionesDisponibles(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar secciones:", error);
|
||||
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'}));
|
||||
setSeccionesDisponibles([]);
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
} else {
|
||||
setSeccionesDisponibles([]);
|
||||
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace.
|
||||
}
|
||||
};
|
||||
if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion
|
||||
fetchSecciones();
|
||||
} else {
|
||||
setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
|
||||
}
|
||||
}, [nuevoEstadoId, idPublicacion]);
|
||||
|
||||
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} required>
|
||||
<InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
|
||||
<Select
|
||||
labelId="nuevo-estado-select-label"
|
||||
label="Nuevo Estado"
|
||||
value={nuevoEstadoId}
|
||||
onChange={(e) => {
|
||||
setNuevoEstadoId(e.target.value as number | string);
|
||||
handleInputChange('nuevoEstadoId');
|
||||
}}
|
||||
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
|
||||
// Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso"
|
||||
useEffect(() => {
|
||||
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
}, [nuevoEstadoId]);
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
||||
if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha 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();
|
||||
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId
|
||||
// y el de sección a un useEffect de idPublicacion
|
||||
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 esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
|
||||
const dataToSubmit: CambiarEstadoBobinaDto = {
|
||||
nuevoEstadoId: Number(nuevoEstadoId),
|
||||
idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
|
||||
idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null,
|
||||
obs: obs.trim() || null,
|
||||
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} required>
|
||||
<InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
|
||||
<Select
|
||||
labelId="nuevo-estado-select-label"
|
||||
label="Nuevo Estado"
|
||||
value={nuevoEstadoId}
|
||||
onChange={(e) => {
|
||||
setNuevoEstadoId(e.target.value as number | string);
|
||||
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} required>
|
||||
<InputLabel id="publicacion-estado-select-label">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 || publicacionesDisponibles.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
|
||||
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
|
||||
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
|
||||
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
|
||||
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion} required>
|
||||
<InputLabel id="seccion-estado-select-label">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 ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : '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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
|
||||
<>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
||||
<InputLabel id="publicacion-estado-select-label">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 || publicacionesDisponibles.length === 0}
|
||||
>
|
||||
<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} required>
|
||||
<InputLabel id="seccion-estado-select-label">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 ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : '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 }}
|
||||
inputProps={{
|
||||
min: bobinaActual?.fechaRemito.split('T')[0]
|
||||
}}
|
||||
/>
|
||||
<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 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO)}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<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>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
{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 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) }>
|
||||
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockBobinaCambioEstadoModal;
|
||||
@@ -1,89 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||
import type { UpdateFechaRemitoLoteDto } from '../../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%', left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 450 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000', boxShadow: 24, p: 4
|
||||
};
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: UpdateFechaRemitoLoteDto) => Promise<void>;
|
||||
bobinaContexto: StockBobinaDto | null;
|
||||
errorMessage: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const StockBobinaFechaRemitoModal: React.FC<Props> = ({ open, onClose, onSubmit, bobinaContexto, errorMessage, clearErrorMessage }) => {
|
||||
const [nuevaFecha, setNuevaFecha] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && bobinaContexto) {
|
||||
setNuevaFecha(bobinaContexto.fechaRemito.split('T')[0]); // Iniciar con la fecha actual
|
||||
setLocalError(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, bobinaContexto, clearErrorMessage]);
|
||||
|
||||
if (!bobinaContexto) return null;
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!nuevaFecha) {
|
||||
setLocalError('Debe seleccionar una nueva fecha.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const data: UpdateFechaRemitoLoteDto = {
|
||||
idPlanta: bobinaContexto.idPlanta,
|
||||
remito: bobinaContexto.remito,
|
||||
fechaRemitoActual: bobinaContexto.fechaRemito.split('T')[0],
|
||||
nuevaFechaRemito: nuevaFecha
|
||||
};
|
||||
await onSubmit(data);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
// El error de la API es manejado por el prop `errorMessage`
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">Corregir Fecha de Remito</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Esto cambiará la fecha para <strong>todas</strong> las bobinas del remito <strong>{bobinaContexto.remito}</strong> en la planta <strong>{bobinaContexto.nombrePlanta}</strong>.
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField label="Fecha Actual" value={new Date(bobinaContexto.fechaRemito).toLocaleDateString('es-AR', { timeZone: 'UTC' })} disabled fullWidth margin="normal" />
|
||||
<TextField label="Nueva Fecha de Remito" type="date" value={nuevaFecha}
|
||||
onChange={e => { setNuevaFecha(e.target.value); setLocalError(null); }}
|
||||
required fullWidth margin="normal" InputLabelProps={{ shrink: true }}
|
||||
error={!!localError} helperText={localError} autoFocus
|
||||
/>
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{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} /> : 'Guardar Nueva Fecha'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockBobinaFechaRemitoModal;
|
||||
@@ -1,374 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, Stepper, Step, StepLabel,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Divider,
|
||||
InputAdornment, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||
import type { CreateStockBobinaLoteDto } from '../../../models/dtos/Impresion/CreateStockBobinaLoteDto';
|
||||
import type { BobinaLoteDetalleDto } from '../../../models/dtos/Impresion/BobinaLoteDetalleDto';
|
||||
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
|
||||
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import stockBobinaService from '../../../services/Impresion/stockBobinaService';
|
||||
import plantaService from '../../../services/Impresion/plantaService';
|
||||
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
|
||||
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%', left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '900px' },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000', boxShadow: 24, p: 3,
|
||||
maxHeight: '90vh', overflowY: 'auto', display: 'flex', flexDirection: 'column'
|
||||
};
|
||||
|
||||
interface NuevaBobinaState extends BobinaLoteDetalleDto {
|
||||
idTemporal: string;
|
||||
}
|
||||
|
||||
interface StockBobinaLoteFormModalProps {
|
||||
open: boolean;
|
||||
onClose: (refrescar: boolean) => void;
|
||||
}
|
||||
|
||||
const steps = ['Datos del Remito', 'Ingreso de Bobinas'];
|
||||
|
||||
const StockBobinaLoteFormModal: React.FC<StockBobinaLoteFormModalProps> = ({ open, onClose }) => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [apiError, setApiError] = useState<string | null>(null);
|
||||
|
||||
// Step 1 State
|
||||
const [idPlanta, setIdPlanta] = useState<number | ''>('');
|
||||
const [remito, setRemito] = useState('');
|
||||
const [fechaRemito, setFechaRemito] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [headerErrors, setHeaderErrors] = useState<{ [key: string]: string }>({});
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [remitoStatusMessage, setRemitoStatusMessage] = useState<string | null>(null);
|
||||
const [remitoStatusSeverity, setRemitoStatusSeverity] = useState<'success' | 'info'>('info');
|
||||
const [isDateAutocompleted, setIsDateAutocompleted] = useState(false);
|
||||
|
||||
// Step 2 State
|
||||
const [bobinasExistentes, setBobinasExistentes] = useState<StockBobinaDto[]>([]);
|
||||
const [nuevasBobinas, setNuevasBobinas] = useState<NuevaBobinaState[]>([]);
|
||||
const [detalleErrors, setDetalleErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
// Dropdowns data
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(true);
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setActiveStep(0); setLoading(false); setApiError(null);
|
||||
setIdPlanta(''); setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]);
|
||||
setHeaderErrors({}); setBobinasExistentes([]); setNuevasBobinas([]); setDetalleErrors({});
|
||||
setRemitoStatusMessage(null);
|
||||
setIsDateAutocompleted(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdowns = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [plantasData, tiposData] = await Promise.all([
|
||||
plantaService.getAllPlantas(),
|
||||
tipoBobinaService.getAllTiposBobina()
|
||||
]);
|
||||
setPlantas(plantasData);
|
||||
setTiposBobina(tiposData);
|
||||
} catch (error) {
|
||||
setApiError("Error al cargar datos necesarios (plantas, tipos).");
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
if (open) {
|
||||
fetchDropdowns();
|
||||
} else {
|
||||
resetState();
|
||||
}
|
||||
}, [open, resetState]);
|
||||
|
||||
useEffect(() => {
|
||||
const verificarRemitoParaAutocompletar = async () => {
|
||||
setRemitoStatusMessage(null);
|
||||
if (remito.trim() && idPlanta) {
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito.trim());
|
||||
if (existentes.length > 0) {
|
||||
setFechaRemito(existentes[0].fechaRemito.split('T')[0]);
|
||||
setRemitoStatusMessage("Remito existente. Se autocompletó la fecha.");
|
||||
setRemitoStatusSeverity('info');
|
||||
setIsDateAutocompleted(true);
|
||||
} else {
|
||||
setRemitoStatusMessage("Este es un remito nuevo.");
|
||||
setRemitoStatusSeverity('success');
|
||||
setIsDateAutocompleted(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fallo la verificación automática de remito: ", error);
|
||||
setRemitoStatusMessage("No se pudo verificar el remito.");
|
||||
setRemitoStatusSeverity('info');
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handler = setTimeout(() => {
|
||||
verificarRemitoParaAutocompletar();
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [idPlanta, remito]);
|
||||
|
||||
const handleClose = () => onClose(false);
|
||||
|
||||
const handleNext = async () => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
if (!remito.trim()) errors.remito = "El número de remito es obligatorio.";
|
||||
if (!idPlanta) errors.idPlanta = "Seleccione una planta.";
|
||||
if (!fechaRemito) errors.fechaRemito = "La fecha es obligatoria.";
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setHeaderErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setApiError(null);
|
||||
try {
|
||||
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito, fechaRemito);
|
||||
setBobinasExistentes(existentes);
|
||||
setActiveStep(1);
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || "Error al verificar el remito.";
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => setActiveStep(0);
|
||||
|
||||
const handleAddBobina = () => {
|
||||
setNuevasBobinas(prev => [...prev, {
|
||||
idTemporal: crypto.randomUUID(), idTipoBobina: 0, nroBobina: '', peso: 0
|
||||
}]);
|
||||
setTimeout(() => {
|
||||
tableContainerRef.current?.scrollTo({ top: tableContainerRef.current.scrollHeight, behavior: 'smooth' });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRemoveBobina = (idTemporal: string) => {
|
||||
setNuevasBobinas(prev => prev.filter(b => b.idTemporal !== idTemporal));
|
||||
};
|
||||
|
||||
const handleBobinaChange = (idTemporal: string, field: keyof NuevaBobinaState, value: any) => {
|
||||
setNuevasBobinas(prev => prev.map(b => b.idTemporal === idTemporal ? { ...b, [field]: value } : b));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
if (nuevasBobinas.length === 0) {
|
||||
setApiError("Debe agregar al menos una nueva bobina para guardar.");
|
||||
return;
|
||||
}
|
||||
|
||||
const todosNrosBobina = new Set(bobinasExistentes.map(b => b.nroBobina));
|
||||
nuevasBobinas.forEach(b => {
|
||||
if (!b.idTipoBobina) errors[b.idTemporal + '_tipo'] = "Requerido";
|
||||
if (!b.nroBobina.trim()) errors[b.idTemporal + '_nro'] = "Requerido";
|
||||
if ((b.peso || 0) <= 0) errors[b.idTemporal + '_peso'] = "Inválido";
|
||||
if (todosNrosBobina.has(b.nroBobina.trim())) errors[b.idTemporal + '_nro'] = "Duplicado";
|
||||
todosNrosBobina.add(b.nroBobina.trim());
|
||||
});
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setDetalleErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setApiError(null);
|
||||
try {
|
||||
const lote: CreateStockBobinaLoteDto = {
|
||||
idPlanta: Number(idPlanta),
|
||||
remito: remito.trim(),
|
||||
fechaRemito,
|
||||
bobinas: nuevasBobinas.map(({ idTipoBobina, nroBobina, peso }) => ({
|
||||
idTipoBobina: Number(idTipoBobina), nroBobina: nroBobina.trim(), peso: Number(peso)
|
||||
}))
|
||||
};
|
||||
await stockBobinaService.ingresarLoteBobinas(lote);
|
||||
onClose(true);
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || "Error al guardar el lote de bobinas.";
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const renderStepContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h6">Datos de Cabecera</Typography>
|
||||
<TextField label="Número de Remito" value={remito}
|
||||
onChange={e => {
|
||||
setRemito(e.target.value);
|
||||
setIsDateAutocompleted(false);
|
||||
}}
|
||||
required error={!!headerErrors.remito} helperText={headerErrors.remito} disabled={loading} autoFocus
|
||||
/>
|
||||
<FormControl fullWidth error={!!headerErrors.idPlanta}>
|
||||
<InputLabel id="planta-label" required>Planta de Destino</InputLabel>
|
||||
<Select labelId="planta-label" value={idPlanta} label="Planta de Destino"
|
||||
onChange={e => {
|
||||
setIdPlanta(e.target.value as number);
|
||||
setIsDateAutocompleted(false);
|
||||
}}
|
||||
disabled={loading || loadingDropdowns}
|
||||
endAdornment={isVerifying && (<InputAdornment position="end" sx={{ mr: 2 }}><CircularProgress size={20} /></InputAdornment>)}
|
||||
>
|
||||
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
{headerErrors.idPlanta && <Typography color="error" variant="caption">{headerErrors.idPlanta}</Typography>}
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Fecha de Remito"
|
||||
type="date"
|
||||
value={fechaRemito}
|
||||
onChange={e => setFechaRemito(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
required
|
||||
error={!!headerErrors.fechaRemito}
|
||||
helperText={headerErrors.fechaRemito}
|
||||
disabled={loading || isDateAutocompleted}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
isDateAutocompleted && (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title="Editar fecha">
|
||||
<IconButton onClick={() => setIsDateAutocompleted(false)} edge="end">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ minHeight: 48, mt: 1 }}>
|
||||
{remitoStatusMessage && !isVerifying && (
|
||||
<Alert severity={remitoStatusSeverity} icon={false} variant="outlined">
|
||||
{remitoStatusMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{bobinasExistentes.length > 0 && (
|
||||
<>
|
||||
<Typography variant="subtitle1" gutterBottom>Bobinas ya ingresadas para este remito:</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: '150px', mb: 2 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead><TableRow><TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell align="right">Peso (Kg)</TableCell></TableRow></TableHead>
|
||||
<TableBody>
|
||||
{bobinasExistentes.map(b => (
|
||||
<TableRow key={b.idBobina}><TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell><TableCell align="right">{b.peso}</TableCell></TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)}
|
||||
<Typography variant="h6">Nuevas Bobinas a Ingresar</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ flexGrow: 1, my: 1, minHeight: '150px' }} ref={tableContainerRef}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead><TableRow><TableCell sx={{ minWidth: 200 }}>Tipo Bobina</TableCell><TableCell>Nro. Bobina</TableCell><TableCell>Peso (Kg)</TableCell><TableCell></TableCell></TableRow></TableHead>
|
||||
<TableBody>
|
||||
{nuevasBobinas.map(bobina => (
|
||||
<TableRow key={bobina.idTemporal}>
|
||||
<TableCell><FormControl fullWidth size="small" error={!!detalleErrors[bobina.idTemporal + '_tipo']}><Select value={bobina.idTipoBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'idTipoBobina', e.target.value)} disabled={loadingDropdowns}><MenuItem value={0} disabled>Seleccione</MenuItem>{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}</Select></FormControl></TableCell>
|
||||
<TableCell><TextField fullWidth size="small" value={bobina.nroBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'nroBobina', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_nro']} helperText={detalleErrors[bobina.idTemporal + '_nro']} /></TableCell>
|
||||
<TableCell><TextField fullWidth size="small" type="number" value={bobina.peso || ''} onChange={e => handleBobinaChange(bobina.idTemporal, 'peso', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_peso']} helperText={detalleErrors[bobina.idTemporal + '_peso']} /></TableCell>
|
||||
<TableCell><IconButton size="small" color="error" onClick={() => handleRemoveBobina(bobina.idTemporal)}><DeleteOutlineIcon /></IconButton></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddBobina} sx={{ mt: 1 }}>Agregar Bobina</Button>
|
||||
</Box>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={handleClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: (theme) => theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom>Ingreso de Bobinas por Lote</Typography>
|
||||
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 2 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||
{loadingDropdowns && activeStep === 0 ? <Box sx={{ display: 'flex', justifyContent: 'center', my: 5 }}><CircularProgress /></Box> : renderStepContent(activeStep)}
|
||||
</Box>
|
||||
|
||||
{apiError && <Alert severity="error" sx={{ mt: 2, flexShrink: 0 }}>{apiError}</Alert>}
|
||||
|
||||
<Divider sx={{ my: 2, flexShrink: 0 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pt: 1, flexShrink: 0 }}>
|
||||
<Button color="inherit" disabled={activeStep === 0 || loading} onClick={handleBack}>
|
||||
Atrás
|
||||
</Button>
|
||||
<Box>
|
||||
{activeStep === 0 && <Button onClick={handleNext} variant="contained" disabled={loading || loadingDropdowns}>{loading ? <CircularProgress size={24} /> : 'Verificar y Continuar'}</Button>}
|
||||
{activeStep === 1 && <Button onClick={handleSubmit} variant="contained" color="success" disabled={loading}>{loading ? <CircularProgress size={24} /> : 'Guardar Lote'}</Button>}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockBobinaLoteFormModal;
|
||||
@@ -41,8 +41,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos") ||
|
||||
moduloLower.includes("cuentas cierres")) {
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
return "Contables";
|
||||
}
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Refleja DTO C# Dtos.Auditoria.CierreCuentaCorrienteHistorialDto.
|
||||
// La tabla _H y este DTO usan PascalCase con underscores; el JSON serializer
|
||||
// default de ASP.NET Core sólo aplica camelCase a la primera letra,
|
||||
// preservando los underscores subsiguientes.
|
||||
export interface CierreCuentaCorrienteHistorialDto {
|
||||
id_Historial: number;
|
||||
id_Cierre: number;
|
||||
id_Distribuidor: number;
|
||||
id_Empresa: number;
|
||||
fechaCorte: string; // ISO datetime
|
||||
fechaCierre: string;
|
||||
saldoCierre: number;
|
||||
estado: 'Activo' | 'Anulado';
|
||||
justificacion?: string | null;
|
||||
id_Usuario_Cierre: number;
|
||||
id_Usuario_Anula?: number | null;
|
||||
fechaAnulacion?: string | null;
|
||||
justificacion_Anulacion?: string | null;
|
||||
|
||||
tipoMod: 'Creacion' | 'Reapertura' | 'Modificacion';
|
||||
id_Usuario_Mod: number;
|
||||
nombreUsuarioModifico: string;
|
||||
fechaMod: string; // ISO datetime
|
||||
}
|
||||
@@ -4,7 +4,4 @@ export interface AjusteSaldoRequestDto {
|
||||
idEmpresa: number;
|
||||
montoAjuste: number;
|
||||
justificacion: string;
|
||||
// Fecha lógica de la operación contable (no la del ajuste físico).
|
||||
// Se valida contra períodos cerrados del distribuidor+empresa.
|
||||
fechaOperacion: string; // "yyyy-MM-dd"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export interface CierreCuentaCorrienteDto {
|
||||
idCierre: number;
|
||||
idDistribuidor: number;
|
||||
nombreDistribuidor: string;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa: string;
|
||||
fechaCorte: string; // "yyyy-MM-dd"
|
||||
fechaCierre: string; // ISO datetime
|
||||
saldoCierre: number;
|
||||
estado: 'Activo' | 'Anulado';
|
||||
justificacion?: string | null;
|
||||
idUsuarioCierre: number;
|
||||
nombreUsuarioCierre: string;
|
||||
idUsuarioAnula?: number | null;
|
||||
nombreUsuarioAnula?: string | null;
|
||||
fechaAnulacion?: string | null; // ISO datetime
|
||||
justificacionAnulacion?: string | null;
|
||||
esUltimoVigente: boolean;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface CrearCierreDto {
|
||||
idDistribuidor: number;
|
||||
idEmpresa: number;
|
||||
fechaCorte: string; // "yyyy-MM-dd"
|
||||
justificacion?: string | null;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface ReabrirCierreDto {
|
||||
// Min 10, max 500 caracteres (validado en backend con DataAnnotations).
|
||||
justificacion: string;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface UltimoCierreDto {
|
||||
idCierre: number;
|
||||
fechaCorte: string; // "yyyy-MM-dd"
|
||||
saldoCierre: number;
|
||||
estado: 'Activo' | 'Anulado';
|
||||
}
|
||||
@@ -12,6 +12,4 @@ export interface DistribuidorDto {
|
||||
telefono?: string | null;
|
||||
email?: string | null;
|
||||
localidad?: string | null;
|
||||
baja?: boolean;
|
||||
fechaBaja?: string | null;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface BobinaLoteDetalleDto {
|
||||
idTipoBobina: number;
|
||||
nroBobina: string;
|
||||
peso: number;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto';
|
||||
|
||||
export interface CreateStockBobinaLoteDto {
|
||||
idPlanta: number;
|
||||
remito: string;
|
||||
fechaRemito: string; // "yyyy-MM-dd"
|
||||
bobinas: BobinaLoteDetalleDto[];
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface UpdateFechaRemitoLoteDto {
|
||||
idPlanta: number;
|
||||
remito: string;
|
||||
fechaRemitoActual: string; // "yyyy-MM-dd"
|
||||
nuevaFechaRemito: string; // "yyyy-MM-dd"
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { BalanceCuentaDebCredDto } from "./BalanceCuentaDebCredDto";
|
||||
import type { BalanceCuentaDistDto } from "./BalanceCuentaDistDto";
|
||||
import type { BalanceCuentaPagosDto } from "./BalanceCuentaPagosDto";
|
||||
import type { SaldoDto } from "./SaldoDto";
|
||||
|
||||
export interface ReporteCuentasDistribuidorResponseDto {
|
||||
entradasSalidas: BalanceCuentaDistDto[];
|
||||
debitosCreditos: BalanceCuentaDebCredDto[];
|
||||
pagos: BalanceCuentaPagosDto[];
|
||||
// Saldo inicial del período: snapshot del último cierre + movimientos netos hasta fechaDesde-1.
|
||||
// 0 si no hay cierre previo.
|
||||
saldoInicial: number;
|
||||
nombreDistribuidor?: string;
|
||||
nombreEmpresa?: string;
|
||||
}
|
||||
saldos: SaldoDto[]; // Aunque SP_BalanceCuentSaldos devuelve una lista, para un distribuidor/empresa debería ser 1 solo.
|
||||
// Se podría ajustar el DTO o el servicio para devolver solo el primer saldo.
|
||||
nombreDistribuidor?: string; // Para el título del reporte
|
||||
nombreEmpresa?: string; // Para el título del reporte
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { BalanceCuentaDebCredDto } from "./BalanceCuentaDebCredDto";
|
||||
import type { BalanceCuentaDistDto } from "./BalanceCuentaDistDto";
|
||||
import type { BalanceCuentaPagosDto } from "./BalanceCuentaPagosDto";
|
||||
import type { SaldoDto } from "./SaldoDto";
|
||||
|
||||
export interface ReporteCuentasDistribuidorResponseDto {
|
||||
entradasSalidas: BalanceCuentaDistDto[];
|
||||
debitosCreditos: BalanceCuentaDebCredDto[];
|
||||
pagos: BalanceCuentaPagosDto[];
|
||||
saldoInicial: number;
|
||||
saldos: SaldoDto[];
|
||||
nombreDistribuidor?: string;
|
||||
nombreEmpresa?: string;
|
||||
}
|
||||
}
|
||||
3
Frontend/src/models/dtos/Reportes/SaldoDto.tsx
Normal file
3
Frontend/src/models/dtos/Reportes/SaldoDto.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface SaldoDto {
|
||||
monto: number;
|
||||
}
|
||||
@@ -62,7 +62,6 @@ const TIPOS_ENTIDAD_AUDITABLES = [
|
||||
{ value: "PermisoMaestro", label: "Permisos (Definición) (gral_Permisos_H)" },
|
||||
{ value: "PermisosPerfiles", label: "Asignación de Permisos a Perfiles (gral_PermisosPerfiles_H)" },
|
||||
{ value: "CambioParada", label: "Cambios de Parada (dist_CambiosParadasCanillas_H)" },
|
||||
{ value: "CierreCC", label: "Cierres de Cuenta Corriente (cue_CierresCuentaCorriente_H)" },
|
||||
].sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const TIPOS_MODIFICACION = [
|
||||
@@ -654,29 +653,6 @@ const AuditoriaGeneralPage: React.FC = () => {
|
||||
{ field: 'vigenciaH', headerName: 'Vig. Hasta', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
|
||||
];
|
||||
break;
|
||||
case "CierreCC":
|
||||
const cierreCcHist = await auditoriaService.getHistorialCierresCC({
|
||||
...commonParams,
|
||||
// "ID Entidad Afectada" filtra por Id_Cierre puntual.
|
||||
idCierreAfectado: filtroIdEntidad ? Number(filtroIdEntidad) : undefined
|
||||
});
|
||||
rawData = cierreCcHist;
|
||||
cols = [
|
||||
...getCommonAuditColumns(),
|
||||
{ field: 'id_Cierre', headerName: 'ID Cierre', width: 100, align: 'center', headerAlign: 'center' },
|
||||
{ field: 'id_Distribuidor', headerName: 'ID Dist.', width: 100, align: 'center', headerAlign: 'center' },
|
||||
{ field: 'id_Empresa', headerName: 'ID Emp.', width: 100, align: 'center', headerAlign: 'center' },
|
||||
{ field: 'fechaCorte', headerName: 'Fecha Corte', width: 120, type: 'date', valueFormatter: (value) => formatDate(value as string) },
|
||||
{ field: 'fechaCierre', headerName: 'Fecha Cierre', width: 170, valueFormatter: (value) => formatDate(value as string) },
|
||||
{ field: 'saldoCierre', headerName: 'Saldo Cierre', width: 140, type: 'number', valueFormatter: (v) => currencyFormatter(v as number) },
|
||||
{ field: 'estado', headerName: 'Estado', width: 100 },
|
||||
{ field: 'justificacion', headerName: 'Justif. Cierre', width: 200, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
|
||||
{ field: 'id_Usuario_Cierre', headerName: 'Usr. Cierre', width: 100, align: 'center', headerAlign: 'center' },
|
||||
{ field: 'id_Usuario_Anula', headerName: 'Usr. Anula', width: 100, align: 'center', headerAlign: 'center', renderCell: (p) => p.value ?? '-' },
|
||||
{ field: 'fechaAnulacion', headerName: 'Fecha Anul.', width: 170, valueFormatter: (v) => v ? formatDate(v as string) : '-' },
|
||||
{ field: 'justificacion_Anulacion', headerName: 'Justif. Anul.', flex: 1, minWidth: 200, renderCell: (params) => (<Tooltip title={params.value || ''}><Typography noWrap variant="body2">{params.value || '-'}</Typography></Tooltip>) },
|
||||
];
|
||||
break;
|
||||
default:
|
||||
setError(`La vista de auditoría para '${filtroTipoEntidad}' aún no está implementada.`);
|
||||
setDatosAuditoria([]);
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Button, Paper, IconButton, Chip, Alert, CircularProgress,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
FormControl, InputLabel, Select, MenuItem, Tabs, Tab, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import cierresCcService from '../../services/Contables/cierresCcService';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
import type { CierreCuentaCorrienteDto } from '../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||
import type { CrearCierreDto } from '../../models/dtos/Contables/CrearCierreDto';
|
||||
import type { ReabrirCierreDto } from '../../models/dtos/Contables/ReabrirCierreDto';
|
||||
import type { CierreCuentaCorrienteHistorialDto } from '../../models/dtos/Auditoria/CierreCuentaCorrienteHistorialDto';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
|
||||
import NuevoCierreModal from '../../components/Modals/Contables/NuevoCierreModal';
|
||||
import ReabrirCierreModal from '../../components/Modals/Contables/ReabrirCierreModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
const datePart = dateString.split('T')[0];
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
return datePart;
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
const d = new Date(dateString);
|
||||
if (isNaN(d.getTime())) return dateString;
|
||||
return d.toLocaleString('es-AR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
};
|
||||
|
||||
// Extrae el codigo de error del backend si viene en el response (PERIODO_CERRADO_BLOQUEO_OPERACION,
|
||||
// CIERRE_FECHA_FUTURA, etc.) y arma un mensaje legible. Si no hay codigo, usa message generico.
|
||||
const parseApiError = (err: unknown, fallback: string): string => {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const data = err.response?.data;
|
||||
if (data && typeof data === 'object') {
|
||||
const codigo = (data as { codigo?: string }).codigo;
|
||||
const mensaje = (data as { mensaje?: string; message?: string }).mensaje
|
||||
|| (data as { mensaje?: string; message?: string }).message;
|
||||
if (mensaje) return codigo ? `[${codigo}] ${mensaje}` : mensaje;
|
||||
if (codigo) return `Error: ${codigo}`;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const CierresCuentaCorrientePage: React.FC = () => {
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVer = isSuperAdmin || tienePermiso('CC003');
|
||||
const puedeCrear = isSuperAdmin || tienePermiso('CC001');
|
||||
// Reapertura: exclusivo SuperAdmin (CC002 no se valida — no es asignable a perfiles).
|
||||
const puedeReabrir = isSuperAdmin;
|
||||
|
||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
|
||||
const [cierres, setCierres] = useState<CierreCuentaCorrienteDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pageApiErrorMessage, setPageApiErrorMessage] = useState<string | null>(null);
|
||||
const [modalApiErrorMessage, setModalApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [cierreSeleccionadoHistorial, setCierreSeleccionadoHistorial] = useState<CierreCuentaCorrienteDto | null>(null);
|
||||
const [historial, setHistorial] = useState<CierreCuentaCorrienteHistorialDto[]>([]);
|
||||
const [loadingHistorial, setLoadingHistorial] = useState(false);
|
||||
|
||||
const [nuevoModalOpen, setNuevoModalOpen] = useState(false);
|
||||
const [reabrirModalOpen, setReabrirModalOpen] = useState(false);
|
||||
const [cierreParaReabrir, setCierreParaReabrir] = useState<CierreCuentaCorrienteDto | null>(null);
|
||||
|
||||
// Carga dropdowns una sola vez al montar
|
||||
useEffect(() => {
|
||||
const fetchDropdowns = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidoresDropdown(),
|
||||
empresaService.getEmpresasDropdown()
|
||||
]);
|
||||
setDistribuidores(distData);
|
||||
setEmpresas(empData);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar opciones de filtro.');
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
fetchDropdowns();
|
||||
}, []);
|
||||
|
||||
const cargarCierres = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError('No tiene permiso.');
|
||||
return;
|
||||
}
|
||||
if (!filtroIdDistribuidor || !filtroIdEmpresa) {
|
||||
setCierres([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setPageApiErrorMessage(null);
|
||||
try {
|
||||
const data = await cierresCcService.getAllCierres(
|
||||
Number(filtroIdDistribuidor),
|
||||
Number(filtroIdEmpresa)
|
||||
);
|
||||
setCierres(data);
|
||||
setPage(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setPageApiErrorMessage(parseApiError(err, 'Error al cargar los cierres.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVer, filtroIdDistribuidor, filtroIdEmpresa]);
|
||||
|
||||
useEffect(() => { cargarCierres(); }, [cargarCierres]);
|
||||
|
||||
const cargarHistorial = useCallback(async (idCierre: number) => {
|
||||
setLoadingHistorial(true);
|
||||
try {
|
||||
const data = await cierresCcService.getHistorialCierre(idCierre);
|
||||
setHistorial(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setPageApiErrorMessage(parseApiError(err, 'Error al cargar el historial.'));
|
||||
} finally {
|
||||
setLoadingHistorial(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectCierreHistorial = (cierre: CierreCuentaCorrienteDto) => {
|
||||
setCierreSeleccionadoHistorial(cierre);
|
||||
setTabIndex(1);
|
||||
cargarHistorial(cierre.idCierre);
|
||||
};
|
||||
|
||||
const clearModalApiErrorMessage = useCallback(() => setModalApiErrorMessage(null), []);
|
||||
|
||||
const handleSubmitNuevoCierre = async (data: CrearCierreDto) => {
|
||||
setModalApiErrorMessage(null);
|
||||
try {
|
||||
const creado = await cierresCcService.crearCierre(data);
|
||||
cargarCierres();
|
||||
return creado;
|
||||
} catch (err) {
|
||||
const msg = parseApiError(err, 'Error al crear el cierre.');
|
||||
setModalApiErrorMessage(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenReabrir = (cierre: CierreCuentaCorrienteDto) => {
|
||||
setCierreParaReabrir(cierre);
|
||||
setReabrirModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmitReabrir = async (idCierre: number, data: ReabrirCierreDto) => {
|
||||
setModalApiErrorMessage(null);
|
||||
try {
|
||||
const result = await cierresCcService.reabrirCierre(idCierre, data);
|
||||
cargarCierres();
|
||||
if (cierreSeleccionadoHistorial?.idCierre === idCierre) {
|
||||
cargarHistorial(idCierre);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const msg = parseApiError(err, 'Error al reabrir el cierre.');
|
||||
setModalApiErrorMessage(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePage = (_e: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const displayData = cierres.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Alert severity="error">No tiene permiso para acceder a esta sección.</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Cierres de Cuenta Corriente</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingDropdowns}>
|
||||
<InputLabel>Distribuidor</InputLabel>
|
||||
<Select
|
||||
value={filtroIdDistribuidor}
|
||||
label="Distribuidor"
|
||||
onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}
|
||||
>
|
||||
<MenuItem value=""><em>Seleccione</em></MenuItem>
|
||||
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingDropdowns}>
|
||||
<InputLabel>Empresa</InputLabel>
|
||||
<Select
|
||||
value={filtroIdEmpresa}
|
||||
label="Empresa"
|
||||
onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}
|
||||
>
|
||||
<MenuItem value=""><em>Seleccione</em></MenuItem>
|
||||
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setNuevoModalOpen(true)}
|
||||
disabled={loadingDropdowns}
|
||||
>
|
||||
Nuevo Cierre
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ mb: 2 }}>
|
||||
<Tabs value={tabIndex} onChange={(_e, v) => setTabIndex(v)}>
|
||||
<Tab label="Cierres" />
|
||||
<Tab
|
||||
label={cierreSeleccionadoHistorial
|
||||
? `Historial #${cierreSeleccionadoHistorial.idCierre}`
|
||||
: 'Historial'}
|
||||
disabled={!cierreSeleccionadoHistorial}
|
||||
icon={<HistoryIcon fontSize="small" />}
|
||||
iconPosition="start"
|
||||
/>
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
{error && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{pageApiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{pageApiErrorMessage}</Alert>}
|
||||
|
||||
{tabIndex === 0 && (
|
||||
<>
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && (!filtroIdDistribuidor || !filtroIdEmpresa) && (
|
||||
<Alert severity="info">Seleccioná un distribuidor y una empresa para ver los cierres.</Alert>
|
||||
)}
|
||||
{!loading && filtroIdDistribuidor && filtroIdEmpresa && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha de Corte</TableCell>
|
||||
<TableCell>Fecha de Cierre</TableCell>
|
||||
<TableCell align="right">Saldo del Cierre</TableCell>
|
||||
<TableCell align="center">Estado</TableCell>
|
||||
<TableCell>Justificación</TableCell>
|
||||
<TableCell>Usuario</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No hay cierres registrados.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCierre} hover>
|
||||
<TableCell>{formatDate(c.fechaCorte)}</TableCell>
|
||||
<TableCell>{formatDateTime(c.fechaCierre)}</TableCell>
|
||||
<TableCell align="right">
|
||||
{c.saldoCierre.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={c.estado === 'Activo' && c.esUltimoVigente ? 'Activo (último)' : c.estado}
|
||||
color={c.estado === 'Activo' ? 'success' : 'default'}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={c.justificacion || ''}>
|
||||
<Box sx={{ maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{c.justificacion || '-'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>{c.nombreUsuarioCierre}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Ver historial">
|
||||
<IconButton size="small" onClick={() => handleSelectCierreHistorial(c)}>
|
||||
<HistoryIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{puedeReabrir && c.estado === 'Activo' && c.esUltimoVigente && (
|
||||
<Tooltip title="Reabrir cierre">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
onClick={() => handleOpenReabrir(c)}
|
||||
>
|
||||
<LockOpenIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]}
|
||||
component="div"
|
||||
count={cierres.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{tabIndex === 1 && cierreSeleccionadoHistorial && (
|
||||
<>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
Historial del cierre <strong>#{cierreSeleccionadoHistorial.idCierre}</strong>
|
||||
{' — '}{cierreSeleccionadoHistorial.nombreDistribuidor} / {cierreSeleccionadoHistorial.nombreEmpresa}
|
||||
{' — '}corte: {formatDate(cierreSeleccionadoHistorial.fechaCorte)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
{loadingHistorial && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{!loadingHistorial && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Tipo Modificación</TableCell>
|
||||
<TableCell>Fecha Modificación</TableCell>
|
||||
<TableCell>Usuario</TableCell>
|
||||
<TableCell align="right">Saldo</TableCell>
|
||||
<TableCell align="center">Estado</TableCell>
|
||||
<TableCell>Justificación</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{historial.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} align="center">No hay registros de auditoría.</TableCell></TableRow>
|
||||
) : (
|
||||
historial.map((h) => (
|
||||
<TableRow key={h.id_Historial}>
|
||||
<TableCell>{h.tipoMod}</TableCell>
|
||||
<TableCell>{formatDateTime(h.fechaMod)}</TableCell>
|
||||
<TableCell>{h.nombreUsuarioModifico}</TableCell>
|
||||
<TableCell align="right">
|
||||
{h.saldoCierre.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip label={h.estado} color={h.estado === 'Activo' ? 'success' : 'default'} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={h.justificacion_Anulacion || h.justificacion || ''}>
|
||||
<Box sx={{ maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{h.justificacion_Anulacion || h.justificacion || '-'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<NuevoCierreModal
|
||||
open={nuevoModalOpen}
|
||||
onClose={() => setNuevoModalOpen(false)}
|
||||
onSubmit={handleSubmitNuevoCierre}
|
||||
initialIdDistribuidor={filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null}
|
||||
initialIdEmpresa={filtroIdEmpresa ? Number(filtroIdEmpresa) : null}
|
||||
errorMessage={modalApiErrorMessage}
|
||||
clearErrorMessage={clearModalApiErrorMessage}
|
||||
/>
|
||||
|
||||
<ReabrirCierreModal
|
||||
open={reabrirModalOpen}
|
||||
onClose={() => { setReabrirModalOpen(false); setCierreParaReabrir(null); }}
|
||||
onSubmit={handleSubmitReabrir}
|
||||
cierre={cierreParaReabrir}
|
||||
errorMessage={modalApiErrorMessage}
|
||||
clearErrorMessage={clearModalApiErrorMessage}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CierresCuentaCorrientePage;
|
||||
@@ -4,12 +4,11 @@ import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
// Define las sub-pestañas del módulo Contables
|
||||
const contablesSubModules = [
|
||||
const contablesSubModules = [
|
||||
{ label: 'Pagos Distribuidores', path: 'pagos-distribuidores' },
|
||||
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
|
||||
{ label: 'Gestión de Saldos', path: 'gestion-saldos' },
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
||||
{ label: 'Cierres CC', path: 'cierres-cc' },
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
||||
];
|
||||
|
||||
const ContablesIndexPage: React.FC = () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
|
||||
import NotaCreditoDebitoFormModal from '../../components/Modals/Contables/NotaCreditoDebitoFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import { parseApiError } from '../../utils/apiErrorParser';
|
||||
import axios from 'axios';
|
||||
|
||||
type DestinoFiltroType = 'Distribuidores' | 'Canillas' | '';
|
||||
type TipoNotaFiltroType = 'Credito' | 'Debito' | '';
|
||||
@@ -124,7 +124,7 @@ const GestionarNotasCDPage: React.FC = () => {
|
||||
}
|
||||
cargarNotas();
|
||||
} catch (err: any) {
|
||||
const { message } = parseApiError(err, 'Error al guardar la nota.');
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la nota.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
@@ -133,7 +133,7 @@ const GestionarNotasCDPage: React.FC = () => {
|
||||
if (window.confirm(`¿Seguro de eliminar esta nota (ID: ${idNota})? Esta acción revertirá el impacto en el saldo.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try { await notaCreditoDebitoService.deleteNota(idNota); cargarNotas(); }
|
||||
catch (err: any) { const { message } = parseApiError(err, 'Error al eliminar.'); setApiErrorMessage(message); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
|
||||
import PagoDistribuidorFormModal from '../../components/Modals/Contables/PagoDistribuidorFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import { parseApiError } from '../../utils/apiErrorParser';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]);
|
||||
@@ -110,7 +110,7 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
}
|
||||
cargarPagos();
|
||||
} catch (err: any) {
|
||||
const { message } = parseApiError(err, 'Error al guardar el pago.');
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.';
|
||||
setModalApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
@@ -121,8 +121,8 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
setPageApiErrorMessage(null);
|
||||
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
|
||||
catch (err: any) {
|
||||
const { message } = parseApiError(err, 'Error al eliminar.');
|
||||
setPageApiErrorMessage(message);
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setPageApiErrorMessage(msg);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
|
||||
import AjusteSaldoModal from '../../components/Modals/Contables/AjusteSaldoModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import { parseApiError } from '../../utils/apiErrorParser';
|
||||
import axios from 'axios';
|
||||
|
||||
type TipoDestinoFiltro = 'Distribuidores' | 'Canillas' | '';
|
||||
|
||||
@@ -48,7 +48,7 @@ const GestionarSaldosPage: React.FC = () => {
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerSaldos = isSuperAdmin || tienePermiso("CS001"); // Permiso para ver
|
||||
const puedeAjustarSaldos = isSuperAdmin; // Ajuste manual: exclusivo SuperAdmin (no se valida CS002)
|
||||
const puedeAjustarSaldos = isSuperAdmin || tienePermiso("CS002"); // Permiso para ajustar
|
||||
|
||||
|
||||
const fetchDropdownData = useCallback(async () => {
|
||||
@@ -128,8 +128,10 @@ const GestionarSaldosPage: React.FC = () => {
|
||||
await saldoService.ajustarSaldo(data);
|
||||
cargarSaldos(); // Recargar lista para ver el saldo actualizado
|
||||
} catch (err: any) {
|
||||
const { message } = parseApiError(err, 'Error al aplicar el ajuste de saldo.');
|
||||
setApiErrorMessage(message);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al aplicar el ajuste de saldo.';
|
||||
setApiErrorMessage(message);
|
||||
throw err; // Para que el modal sepa que hubo error
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// src/pages/Distribucion/GestionarDistribuidoresPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel, ListItemIcon, ListItemText
|
||||
CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import TrashIcon from '@mui/icons-material/Delete';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto';
|
||||
@@ -26,7 +24,6 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroNroDoc, setFiltroNroDoc] = useState('');
|
||||
const [filtroSoloActivos, setFiltroSoloActivos] = useState<boolean | undefined>(true);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null);
|
||||
@@ -52,12 +49,12 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc, filtroSoloActivos);
|
||||
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc);
|
||||
setDistribuidores(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los distribuidores.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, filtroNroDoc, filtroSoloActivos, puedeVer]);
|
||||
}, [filtroNombre, filtroNroDoc, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]);
|
||||
|
||||
@@ -97,21 +94,6 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleToggleBaja = async (distribuidor: DistribuidorDto) => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = distribuidor.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${distribuidor.nombre}?`)) {
|
||||
try {
|
||||
await distribuidorService.toggleBajaDistribuidor(distribuidor.idDistribuidor, { darDeBaja: !distribuidor.baja, fechaBaja: !distribuidor.baja ? new Date().toISOString() : null });
|
||||
cargarDistribuidores();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el distribuidor.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, distribuidor: DistribuidorDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor);
|
||||
};
|
||||
@@ -150,17 +132,7 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
onChange={(e) => setFiltroNroDoc(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Ver Activos"
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
|
||||
@@ -178,7 +150,6 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
<TableCell>Nombre</TableCell><TableCell>Nro. Doc.</TableCell>
|
||||
<TableCell>Contacto</TableCell><TableCell>Zona</TableCell>
|
||||
<TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell>
|
||||
<TableCell>Estado</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
@@ -186,11 +157,10 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((d) => (
|
||||
<TableRow key={d.idDistribuidor} hover sx={{ backgroundColor: d.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableRow key={d.idDistribuidor} hover>
|
||||
<TableCell>{d.nombre}</TableCell><TableCell>{d.nroDoc}</TableCell>
|
||||
<TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell>
|
||||
<TableCell>{d.telefono || '-'}</TableCell><TableCell>{d.localidad || '-'}</TableCell>
|
||||
<TableCell>{d.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
@@ -209,24 +179,8 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && selectedDistribuidorRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && selectedDistribuidorRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedDistribuidorRow)}>
|
||||
<ListItemIcon>{selectedDistribuidorRow.baja ? <ToggleOnIcon fontSize="small" /> : <ToggleOffIcon fontSize="small" />}</ListItemIcon>
|
||||
<ListItemText>{selectedDistribuidorRow.baja ? 'Reactivar' : 'Dar de Baja'}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && selectedDistribuidorRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedDistribuidorRow.idDistribuidor)}>
|
||||
<ListItemIcon><TrashIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar (Físico)</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} />Modificar</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}><TrashIcon fontSize="small" sx={{ mr: 1 }} />Eliminar</MenuItem>)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
setLoadingFiltersDropdown(false); // Detiene el spinner de los filtros
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setLoadingFiltersDropdown(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -130,14 +130,14 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
|
||||
const cargarMovimientos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
@@ -164,37 +164,37 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||
cargarMovimientos();
|
||||
cargarMovimientos();
|
||||
} else {
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false);
|
||||
}
|
||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
if (!puedeCrear && !item) {
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
}
|
||||
if (item && !puedeModificar) {
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
} else {
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe,
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe,
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
@@ -224,7 +224,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
if (event.target.checked) {
|
||||
const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
|
||||
setSelectedIdsParaLiquidar(newSelectedIds);
|
||||
} else {
|
||||
@@ -248,17 +248,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
|
||||
let fechaMovimientoMasReciente: Date | null = null;
|
||||
selectedIdsParaLiquidar.forEach(idParte => {
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', { timeZone: 'UTC' })}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', { timeZone: 'UTC' })}).`);
|
||||
return;
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`);
|
||||
return;
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setLoading(true);
|
||||
@@ -277,9 +277,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
movimientoParaTicket.fecha,
|
||||
false
|
||||
movimientoParaTicket.idCanilla,
|
||||
movimientoParaTicket.fecha,
|
||||
false
|
||||
);
|
||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
|
||||
@@ -344,13 +344,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
const totalARendirVisible = useMemo(() =>
|
||||
const totalARendirVisible = useMemo(() =>
|
||||
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0)
|
||||
, [displayData]);
|
||||
|
||||
const montoARendirAll = useMemo(() =>
|
||||
movimientos.reduce((sum, item) => sum + item.montoARendir, 0)
|
||||
, [movimientos]);
|
||||
, [displayData]);
|
||||
|
||||
if (!puedeVer) {
|
||||
return (
|
||||
@@ -371,7 +367,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
||||
required
|
||||
@@ -402,9 +398,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)}
|
||||
>
|
||||
<MenuItem value=""><em>Seleccione uno</em></MenuItem>
|
||||
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})` : ''}</MenuItem>)}
|
||||
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})`: ''}</MenuItem>)}
|
||||
</Select>
|
||||
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ ml: 1.5, fontSize: '0.65rem' }}>Selección obligatoria</Typography>}
|
||||
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ml:1.5, fontSize:'0.65rem'}}>Selección obligatoria</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
@@ -415,15 +411,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap:2 }}>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado}
|
||||
>
|
||||
Registrar Movimiento
|
||||
Registrar Movimiento
|
||||
</Button>
|
||||
)}
|
||||
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
|
||||
@@ -433,39 +429,32 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{!filtroFecha && <Alert severity="info" sx={{ my: 1 }}>Por favor, seleccione una fecha.</Alert>}
|
||||
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{ my: 1 }}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
|
||||
|
||||
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</Alert>}
|
||||
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{my:1}}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{loadingTicketPdf && (<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}> <CircularProgress size={20} sx={{ mr: 1 }} /> <Typography variant="body2">Cargando ticket...</Typography> </Box>)}
|
||||
{loadingTicketPdf && ( <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}> <CircularProgress size={20} sx={{ mr: 1 }} /> <Typography variant="body2">Cargando ticket...</Typography> </Box> )}
|
||||
|
||||
|
||||
{!loading && movimientos.length > 0 && (
|
||||
<Paper sx={{ p: 1.5, mb: 2, mt: 1, backgroundColor: 'grey.100' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 1 }}>
|
||||
Total:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ paddingRight: '5px', fontWeight: 'bold', color: 'text.main' }}>
|
||||
{montoARendirAll.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
-
|
||||
<Typography variant="subtitle1" sx={{ mr: 1, paddingLeft: '5px', }}>
|
||||
Total a Liquidar:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: totalARendirVisible > 0 ? 'error.main' : 'green' }}>
|
||||
{totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Paper sx={{ p: 1.5, mb: 2, mt:1, backgroundColor: 'grey.100' }}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
|
||||
<Typography variant="subtitle1" sx={{mr:2}}>
|
||||
Total a Liquidar:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{fontWeight: 'bold', color: 'error.main'}}>
|
||||
{totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{puedeLiquidar && (
|
||||
@@ -556,10 +545,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && selectedRow && !selectedRow.liquidado && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
|
||||
|
||||
{selectedRow && selectedRow.liquidado && puedeLiquidar && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@@ -571,7 +560,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
selectedRow.canillaEsAccionista
|
||||
);
|
||||
}
|
||||
handleMenuClose();
|
||||
handleMenuClose();
|
||||
}}
|
||||
disabled={loadingTicketPdf}
|
||||
>
|
||||
@@ -583,9 +572,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
{selectedRow && (
|
||||
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
|
||||
) && (
|
||||
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
Eliminar
|
||||
<MenuItem onClick={() => {if (selectedRow) handleDelete(selectedRow.idParte);}}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
@@ -600,7 +589,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<DialogTitle>Confirmar Liquidación</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/Dis
|
||||
|
||||
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import { parseApiError } from '../../utils/apiErrorParser';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
const [movimientos, setMovimientos] = useState<EntradaSalidaDistDto[]>([]);
|
||||
@@ -115,7 +115,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
}
|
||||
cargarMovimientos();
|
||||
} catch (err: any) {
|
||||
const { message } = parseApiError(err, 'Error al guardar.');
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
@@ -124,7 +124,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
if (window.confirm(`¿Seguro (ID: ${idParte})? Esto revertirá el saldo.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try { await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); cargarMovimientos(); }
|
||||
catch (err: any) { const { message } = parseApiError(err, 'Error al eliminar.'); setApiErrorMessage(message); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, type GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
@@ -13,7 +11,6 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import EditCalendarIcon from '@mui/icons-material/EditCalendar';
|
||||
|
||||
import stockBobinaService from '../../services/Impresion/stockBobinaService';
|
||||
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
|
||||
@@ -27,13 +24,10 @@ import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/Cambiar
|
||||
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
import type { UpdateFechaRemitoLoteDto } from '../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||
|
||||
import StockBobinaFechaRemitoModal from '../../components/Modals/Impresion/StockBobinaFechaRemitoModal';
|
||||
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
|
||||
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
|
||||
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
|
||||
import StockBobinaLoteFormModal from '../../components/Modals/Impresion/StockBobinaLoteFormModal';
|
||||
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
@@ -44,27 +38,20 @@ const ID_ESTADO_DANADA = 3;
|
||||
|
||||
const GestionarStockBobinasPage: React.FC = () => {
|
||||
const [stock, setStock] = useState<StockBobinaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false); // No carga al inicio
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// --- Estados de los filtros ---
|
||||
// Estados de los filtros
|
||||
const [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
|
||||
const [filtroNroBobina, setFiltroNroBobina] = useState('');
|
||||
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
|
||||
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
|
||||
const [filtroRemito, setFiltroRemito] = useState('');
|
||||
|
||||
// Filtro Fechas Remito
|
||||
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false);
|
||||
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false); // <-- NUEVO
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
// Nuevo Filtro: Fechas Estado
|
||||
const [filtroFechaEstadoHabilitado, setFiltroFechaEstadoHabilitado] = useState<boolean>(false);
|
||||
const [filtroFechaEstadoDesde, setFiltroFechaEstadoDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaEstadoHasta, setFiltroFechaEstadoHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
// Estados para datos de dropdowns
|
||||
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
@@ -75,10 +62,13 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
|
||||
const [loteModalOpen, setLoteModalOpen] = useState(false);
|
||||
const [fechaRemitoModalOpen, setFechaRemitoModalOpen] = useState(false);
|
||||
|
||||
// Menú de acciones
|
||||
// Estado para la bobina seleccionada en un modal o menú
|
||||
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
|
||||
|
||||
// Estados para la paginación y el menú de acciones
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null);
|
||||
|
||||
@@ -89,9 +79,12 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
|
||||
|
||||
const lastOpenedMenuButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
// Asumiendo que estos servicios existen y devuelven los DTOs correctos
|
||||
const [tiposData, plantasData, estadosData] = await Promise.all([
|
||||
tipoBobinaService.getAllTiposBobina(),
|
||||
plantaService.getAllPlantas(),
|
||||
@@ -128,18 +121,13 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
idPlanta: filtroPlanta ? Number(filtroPlanta) : null,
|
||||
idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null,
|
||||
remitoFilter: filtroRemito || null,
|
||||
// Fechas Remito
|
||||
fechaDesde: filtroFechaHabilitado ? filtroFechaDesde : null,
|
||||
fechaHasta: filtroFechaHabilitado ? filtroFechaHasta : null,
|
||||
// Fechas Estado (Nuevos parametros, asegurar que el backend los reciba)
|
||||
fechaEstadoDesde: filtroFechaEstadoHabilitado ? filtroFechaEstadoDesde : null,
|
||||
fechaEstadoHasta: filtroFechaEstadoHabilitado ? filtroFechaEstadoHasta : null,
|
||||
};
|
||||
const data = await stockBobinaService.getAllStockBobinas(params);
|
||||
setStock(data);
|
||||
if (data.length === 0) {
|
||||
// No setteamos error bloqueante, solo aviso visual si se desea, o dejar tabla vacía.
|
||||
// setError("No se encontraron resultados con los filtros aplicados.");
|
||||
setError("No se encontraron resultados con los filtros aplicados.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -147,14 +135,10 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
puedeVer,
|
||||
filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito,
|
||||
filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta,
|
||||
filtroFechaEstadoHabilitado, filtroFechaEstadoDesde, filtroFechaEstadoHasta
|
||||
]);
|
||||
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]);
|
||||
|
||||
const handleBuscarClick = () => {
|
||||
setPage(0); // Resetear la paginación al buscar
|
||||
cargarStock();
|
||||
};
|
||||
|
||||
@@ -164,19 +148,14 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
setFiltroPlanta('');
|
||||
setFiltroEstadoBobina('');
|
||||
setFiltroRemito('');
|
||||
|
||||
setFiltroFechaHabilitado(false);
|
||||
setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
|
||||
setFiltroFechaHasta(new Date().toISOString().split('T')[0]);
|
||||
|
||||
setFiltroFechaEstadoHabilitado(false);
|
||||
setFiltroFechaEstadoDesde(new Date().toISOString().split('T')[0]);
|
||||
setFiltroFechaEstadoHasta(new Date().toISOString().split('T')[0]);
|
||||
|
||||
setStock([]);
|
||||
setStock([]); // Limpiar los resultados actuales
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
|
||||
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
|
||||
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
@@ -184,166 +163,92 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
|
||||
};
|
||||
|
||||
const handleLoteModalClose = (refrescar: boolean) => {
|
||||
setLoteModalOpen(false);
|
||||
if (refrescar) {
|
||||
cargarStock();
|
||||
const handleOpenEditModal = (bobina: StockBobinaDto | null) => {
|
||||
if (!bobina) return;
|
||||
setSelectedBobina(bobina);
|
||||
setApiErrorMessage(null);
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
const handleCloseEditModal = () => {
|
||||
setEditModalOpen(false);
|
||||
setSelectedBobina(null);
|
||||
if (lastOpenedMenuButtonRef.current) {
|
||||
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; }
|
||||
};
|
||||
|
||||
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto | null) => {
|
||||
if (!bobina) return;
|
||||
setSelectedBobina(bobina);
|
||||
setApiErrorMessage(null);
|
||||
setCambioEstadoModalOpen(true);
|
||||
};
|
||||
const handleCloseCambioEstadoModal = () => setCambioEstadoModalOpen(false);
|
||||
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; }
|
||||
};
|
||||
|
||||
const handleDeleteBobina = () => {
|
||||
if (!selectedBobinaForRowMenu) return;
|
||||
|
||||
if (selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DISPONIBLE && selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DANADA) {
|
||||
const handleDeleteBobina = async (bobina: StockBobinaDto | null) => {
|
||||
if (!bobina) return;
|
||||
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
|
||||
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
|
||||
handleMenuClose();
|
||||
return;
|
||||
}
|
||||
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${selectedBobinaForRowMenu.idBobina})?`)) {
|
||||
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${bobina.idBobina})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
stockBobinaService.deleteIngresoBobina(selectedBobinaForRowMenu.idBobina)
|
||||
.then(() => cargarStock())
|
||||
.catch((err: any) => {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setApiErrorMessage(msg);
|
||||
});
|
||||
try { await stockBobinaService.deleteIngresoBobina(bobina.idBobina); cargarStock(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleSubmitFechaRemitoModal = async (data: UpdateFechaRemitoLoteDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await stockBobinaService.actualizarFechaRemitoLote(data);
|
||||
cargarStock();
|
||||
} catch (err: any) {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar la fecha del remito.';
|
||||
setApiErrorMessage(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Handlers Menú Acciones ---
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
|
||||
event.stopPropagation(); // Evitar selección de fila al abrir menú
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedBobinaForRowMenu(bobina);
|
||||
lastOpenedMenuButtonRef.current = event.currentTarget;
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedBobinaForRowMenu(null);
|
||||
if (lastOpenedMenuButtonRef.current) {
|
||||
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenEditModal = () => { setEditModalOpen(true); handleMenuClose(); };
|
||||
const handleOpenCambioEstadoModal = () => { setCambioEstadoModalOpen(true); handleMenuClose(); };
|
||||
const handleOpenFechaRemitoModal = () => { setFechaRemitoModalOpen(true); handleMenuClose(); };
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
|
||||
const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobinaForRowMenu(null); };
|
||||
const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobinaForRowMenu(null); };
|
||||
const handleCloseFechaRemitoModal = () => { setFechaRemitoModalOpen(false); setSelectedBobinaForRowMenu(null); };
|
||||
|
||||
// --- Definición de Columnas DataGrid ---
|
||||
const columns = useMemo<GridColDef<StockBobinaDto>[]>(() => [
|
||||
{ field: 'nroBobina', headerName: 'Nro. Bobina', width: 130 },
|
||||
{ field: 'nombreTipoBobina', headerName: 'Tipo', width: 200, flex: 1 },
|
||||
{ field: 'peso', headerName: 'Peso (Kg)', width: 100, align: 'right', headerAlign: 'right', type: 'number' },
|
||||
{ field: 'nombrePlanta', headerName: 'Planta', width: 120 },
|
||||
{
|
||||
field: 'nombreEstadoBobina',
|
||||
headerName: 'Estado',
|
||||
width: 130,
|
||||
renderCell: (params) => {
|
||||
const idEstado = params.row.idEstadoBobina;
|
||||
let color: "success" | "primary" | "error" | "default" = "default";
|
||||
if (idEstado === ID_ESTADO_DISPONIBLE) color = "success";
|
||||
else if (idEstado === ID_ESTADO_UTILIZADA) color = "primary";
|
||||
else if (idEstado === ID_ESTADO_DANADA) color = "error";
|
||||
|
||||
return <Chip label={params.value} size="small" color={color} variant="outlined" />;
|
||||
}
|
||||
},
|
||||
{ field: 'remito', headerName: 'Remito', width: 120 },
|
||||
{
|
||||
field: 'fechaRemito',
|
||||
headerName: 'F. Remito',
|
||||
width: 110,
|
||||
type: 'date',
|
||||
valueGetter: (value: string) => {
|
||||
if (!value) return null;
|
||||
const datePart = value.toString().split('T')[0];
|
||||
const [year, month, day] = datePart.split('-');
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
},
|
||||
valueFormatter: (value: Date) => {
|
||||
return value ? value.toLocaleDateString('es-AR') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'fechaEstado',
|
||||
headerName: 'F. Estado',
|
||||
width: 110,
|
||||
type: 'date',
|
||||
valueGetter: (value: string) => {
|
||||
if (!value) return null;
|
||||
const datePart = value.toString().split('T')[0];
|
||||
const [year, month, day] = datePart.split('-');
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
},
|
||||
valueFormatter: (value: Date) => {
|
||||
return value ? value.toLocaleDateString('es-AR') : '-';
|
||||
}
|
||||
},
|
||||
|
||||
{ field: 'nombrePublicacion', headerName: 'Publicación', width: 150 },
|
||||
{ field: 'nombreSeccion', headerName: 'Sección', width: 120 },
|
||||
{ field: 'obs', headerName: 'Obs.', width: 200, flex: 1 },
|
||||
{
|
||||
field: 'acciones',
|
||||
headerName: 'Acciones',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
align: 'right',
|
||||
renderCell: (params: GridRenderCellParams<StockBobinaDto>) => {
|
||||
const b = params.row;
|
||||
const disabled = !(puedeModificarDatos) &&
|
||||
!(puedeCambiarEstado) &&
|
||||
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar);
|
||||
|
||||
if (disabled) return null;
|
||||
|
||||
return (
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, b)} size="small">
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
], [puedeModificarDatos, puedeCambiarEstado, puedeEliminar]);
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
timeZone: 'UTC'
|
||||
};
|
||||
return new Intl.DateTimeFormat('es-AR', options).format(date);
|
||||
};
|
||||
|
||||
if (!puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Stock de Bobinas</Typography>
|
||||
|
||||
{/* Panel de Filtros */}
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros</Typography>
|
||||
|
||||
{/* Fila 1: Filtros generales */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
|
||||
<InputLabel>Tipo Bobina</InputLabel>
|
||||
@@ -369,123 +274,128 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
</FormControl>
|
||||
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} />
|
||||
</Box>
|
||||
|
||||
{/* Fila 2: Filtros de Fechas */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4, mb: 2, alignItems: 'center' }}>
|
||||
{/* Fechas Remito */}
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', border: '1px dashed #ccc', p: 1, borderRadius: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filtroFechaHabilitado} onChange={(e) => setFiltroFechaHabilitado(e.target.checked)} />}
|
||||
label="Filtrar por Fecha Remito"
|
||||
/>
|
||||
<TextField label="Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaHabilitado} />
|
||||
<TextField label="Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaHabilitado} />
|
||||
</Box>
|
||||
|
||||
{/* Fechas Estado (Nuevo) */}
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', border: '1px dashed #ccc', p: 1, borderRadius: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filtroFechaEstadoHabilitado} onChange={(e) => setFiltroFechaEstadoHabilitado(e.target.checked)} />}
|
||||
label="Filtrar por Fecha Estado"
|
||||
/>
|
||||
<TextField label="Desde" type="date" size="small" value={filtroFechaEstadoDesde} onChange={(e) => setFiltroFechaEstadoDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaEstadoHabilitado} />
|
||||
<TextField label="Hasta" type="date" size="small" value={filtroFechaEstadoHasta} onChange={(e) => setFiltroFechaEstadoHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaEstadoHabilitado} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filtroFechaHabilitado} onChange={(e) => setFiltroFechaHabilitado(e.target.checked)} />}
|
||||
label="Filtrar por Fechas de Remitos"
|
||||
/>
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} />
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} />
|
||||
</Box>
|
||||
|
||||
{/* Botones de acción del filtro */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2, mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>
|
||||
Buscar
|
||||
</Button>
|
||||
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>
|
||||
Limpiar Filtros
|
||||
</Button>
|
||||
</Box>
|
||||
{puedeIngresar && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button variant="contained" color="secondary" startIcon={<AddIcon />} onClick={() => setLoteModalOpen(true)}>
|
||||
Ingreso por Remito (Lote)
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>Buscar</Button>
|
||||
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>Limpiar Filtros</Button>
|
||||
</Box>
|
||||
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ ml: 'auto' }}>Ingresar Bobina</Button>)}
|
||||
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="warning" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{/* Tabla DataGrid */}
|
||||
<Paper sx={{ width: '100%', height: 600 }}>
|
||||
<DataGrid
|
||||
rows={stock}
|
||||
columns={columns}
|
||||
getRowId={(row) => row.idBobina} // Importante: especificar el ID único
|
||||
loading={loading}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
disableRowSelectionOnClick
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
sx={{ border: 0 }}
|
||||
/>
|
||||
</Paper>
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell>
|
||||
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell>
|
||||
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
|
||||
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
|
||||
<TableCell>Obs.</TableCell>
|
||||
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) ? 12 : 11} align="center">No se encontraron bobinas con los filtros aplicados. Haga clic en "Buscar" para iniciar una consulta.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((b) => (
|
||||
<TableRow key={b.idBobina} hover>
|
||||
<TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell>
|
||||
<TableCell align="right">{b.peso}</TableCell><TableCell>{b.nombrePlanta}</TableCell>
|
||||
<TableCell><Chip label={b.nombreEstadoBobina} size="small" color={
|
||||
b.idEstadoBobina === ID_ESTADO_DISPONIBLE ? "success" : b.idEstadoBobina === ID_ESTADO_UTILIZADA ? "primary" : b.idEstadoBobina === ID_ESTADO_DANADA ? "error" : "default"
|
||||
} /></TableCell>
|
||||
<TableCell>{b.remito}</TableCell><TableCell>{formatDate(b.fechaRemito)}</TableCell>
|
||||
<TableCell>{formatDate(b.fechaEstado)}</TableCell>
|
||||
<TableCell>{b.nombrePublicacion || '-'}</TableCell><TableCell>{b.nombreSeccion || '-'}</TableCell>
|
||||
<TableCell>{b.obs || '-'}</TableCell>
|
||||
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, b)}
|
||||
disabled={
|
||||
!(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) &&
|
||||
!(puedeCambiarEstado) &&
|
||||
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)
|
||||
}
|
||||
><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={stock.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{/* Menú Contextual de Fila */}
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{selectedBobinaForRowMenu && puedeModificarDatos && (
|
||||
<MenuItem onClick={handleOpenFechaRemitoModal}>
|
||||
<EditCalendarIcon fontSize="small" sx={{ mr: 1 }} /> Corregir Fecha Remito
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && (
|
||||
<MenuItem onClick={handleOpenEditModal}>
|
||||
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos Bobina
|
||||
<MenuItem onClick={() => { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
|
||||
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu && puedeCambiarEstado && (
|
||||
<MenuItem onClick={handleOpenCambioEstadoModal}>
|
||||
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
|
||||
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu && puedeEliminar &&
|
||||
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && (
|
||||
<MenuItem onClick={handleDeleteBobina}>
|
||||
<MenuItem onClick={() => handleDeleteBobina(selectedBobinaForRowMenu)}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu &&
|
||||
!((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) &&
|
||||
!(puedeCambiarEstado) &&
|
||||
!(((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)) &&
|
||||
<MenuItem disabled>Sin acciones disponibles</MenuItem>
|
||||
}
|
||||
</Menu>
|
||||
|
||||
{/* Modales */}
|
||||
{/* Modales sin cambios */}
|
||||
<StockBobinaIngresoFormModal
|
||||
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
|
||||
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
<StockBobinaEditFormModal
|
||||
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
|
||||
initialData={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
<StockBobinaCambioEstadoModal
|
||||
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
|
||||
bobinaActual={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
<StockBobinaLoteFormModal
|
||||
open={loteModalOpen}
|
||||
onClose={handleLoteModalClose}
|
||||
/>
|
||||
<StockBobinaFechaRemitoModal
|
||||
open={fechaRemitoModalOpen}
|
||||
onClose={handleCloseFechaRemitoModal}
|
||||
onSubmit={handleSubmitFechaRemitoModal}
|
||||
bobinaContexto={selectedBobinaForRowMenu}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
{editModalOpen && selectedBobina &&
|
||||
<StockBobinaEditFormModal
|
||||
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
|
||||
initialData={selectedBobina} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
{cambioEstadoModalOpen && selectedBobina &&
|
||||
<StockBobinaCambioEstadoModal
|
||||
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
|
||||
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,9 +63,8 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const baseInicial = data.saldoInicial ?? 0;
|
||||
const movs = procesarLista(data.entradasSalidas, 'mov', baseInicial);
|
||||
const ultimoMov = movs.length ? movs[movs.length - 1].saldoAcumulado : baseInicial;
|
||||
const movs = procesarLista(data.entradasSalidas, 'mov', 0);
|
||||
const ultimoMov = movs.length ? movs[movs.length - 1].saldoAcumulado : 0;
|
||||
const notas = procesarLista(data.debitosCreditos, 'nota', ultimoMov);
|
||||
const ultimoNota = notas.length ? notas[notas.length - 1].saldoAcumulado : ultimoMov;
|
||||
const pagos = procesarLista(data.pagos, 'pago', ultimoNota);
|
||||
@@ -211,7 +210,7 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
<Box sx={{ p: 1, fontWeight: 'bold' }}>
|
||||
<Typography variant="subtitle2" component="span" sx={{ mr: 2 }}>
|
||||
TOTAL DEBE: <strong>{totalDebe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
TOTA DEBE: <strong>{totalDebe.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" component="span">
|
||||
TOTAL HABER: <strong>{totalHaber.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
@@ -285,44 +284,39 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!originalReportData) {
|
||||
alert('No hay datos para exportar.');
|
||||
return;
|
||||
if (
|
||||
!originalReportData ||
|
||||
(movimientosConSaldo.length === 0 &&
|
||||
notasConSaldo.length === 0 &&
|
||||
pagosConSaldo.length === 0)
|
||||
) {
|
||||
alert("No hay datos para exportar."); // O un mensaje más amigable
|
||||
return;
|
||||
}
|
||||
|
||||
const sumDebe = (rows: Array<{ debe?: number }>) => rows.reduce((s, r) => s + (r.debe || 0), 0);
|
||||
const sumHaber = (rows: Array<{ haber?: number }>) => rows.reduce((s, r) => s + (r.haber || 0), 0);
|
||||
const wb = XLSX.utils.book_new();// Se crea un nuevo libro
|
||||
|
||||
const totalDebeAll = sumDebe(movimientosConSaldo) + sumDebe(notasConSaldo) + sumDebe(pagosConSaldo);
|
||||
const totalHaberAll = sumHaber(movimientosConSaldo) + sumHaber(notasConSaldo) + sumHaber(pagosConSaldo);
|
||||
const saldoInicialExp = originalReportData.saldoInicial ?? 0;
|
||||
const saldoFinalExp = saldoInicialExp + totalDebeAll - totalHaberAll;
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Hoja Resumen — siempre presente, con saldo inicial y final aunque no haya movimientos
|
||||
const resumenRows = [
|
||||
{ Concepto: 'Saldo Inicial', Monto: saldoInicialExp },
|
||||
{ Concepto: 'Total Debe', Monto: totalDebeAll },
|
||||
{ Concepto: 'Total Haber', Monto: totalHaberAll },
|
||||
{ Concepto: 'Saldo Final', Monto: saldoFinalExp },
|
||||
];
|
||||
const wsResumen = XLSX.utils.json_to_sheet(resumenRows);
|
||||
XLSX.utils.book_append_sheet(wb, wsResumen, 'Resumen');
|
||||
|
||||
if (movimientosConSaldo.length) {
|
||||
// Movimientos
|
||||
if (movimientosConSaldo.length) { // <--- CHEQUEO 1
|
||||
// Si movimientosConSaldo está vacío, esta hoja no se añade
|
||||
const ws = XLSX.utils.json_to_sheet(movimientosConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Movimientos');
|
||||
}
|
||||
if (notasConSaldo.length) {
|
||||
// Notas
|
||||
if (notasConSaldo.length) { // <--- CHEQUEO 2
|
||||
// Si notasConSaldo está vacío, esta hoja no se añade
|
||||
const ws = XLSX.utils.json_to_sheet(notasConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Notas');
|
||||
}
|
||||
if (pagosConSaldo.length) {
|
||||
// Pagos
|
||||
if (pagosConSaldo.length) { // <--- CHEQUEO 3
|
||||
// Si pagosConSaldo está vacío, esta hoja no se añade
|
||||
const ws = XLSX.utils.json_to_sheet(pagosConSaldo.map(({ id, ...rest }) => rest));
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Pagos');
|
||||
}
|
||||
|
||||
// Si ninguno de los arrays tiene datos, el libro 'wb' quedará vacío.
|
||||
// Y la siguiente línea dará el error:
|
||||
XLSX.writeFile(wb, `Reporte_${Date.now()}.xlsx`);
|
||||
}, [originalReportData, movimientosConSaldo, notasConSaldo, pagosConSaldo]);
|
||||
|
||||
@@ -361,8 +355,7 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const totalMov = movimientosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||
const totalNot = notasConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||
const totalPag = pagosConSaldo.reduce((sum, r) => sum + ((r.debe || 0) - (r.haber || 0)), 0);
|
||||
const saldoInicial = originalReportData?.saldoInicial ?? 0;
|
||||
const saldoFinal = saldoInicial + totalMov + totalNot + totalPag;
|
||||
const saldoInicial = originalReportData?.saldos?.[0]?.monto || 0;
|
||||
|
||||
const cols = generarColumns();
|
||||
|
||||
@@ -392,7 +385,7 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
<Typography>Distribuidor: <strong>{currentParams?.nombreDistribuidor}</strong></Typography>
|
||||
<Typography>Empresa: <strong>{currentParams?.nombreEmpresa}</strong></Typography>
|
||||
<Typography>Período: <strong>{currentParams?.fechaDesde}</strong> al <strong>{currentParams?.fechaHasta}</strong></Typography>
|
||||
<Typography>Saldo Inicial del Período: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
||||
<Typography>Saldo a la Fecha {new Date().toLocaleDateString('es-AR')}: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
||||
</Paper>
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 2 }}>Movimientos de Entrada / Salida</Typography>
|
||||
@@ -406,12 +399,11 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
|
||||
<Paper sx={{ p: 2, mt: 3 }}>
|
||||
<Typography variant="h6">Resumen Final</Typography>
|
||||
<Typography>Saldo Inicial: <strong>{saldoInicial.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong></Typography>
|
||||
<Typography>Movimientos (Debe - Haber): {totalMov.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||
<Typography>Notas C/D (Debe - Haber): {totalNot.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||
<Typography>Pagos (Debe - Haber): {totalPag.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mt: 1 }}>
|
||||
Saldo Final: {saldoFinal.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
Saldo Final del Período: {(totalMov + totalNot + totalPag).toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -69,8 +69,8 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
setReportData(null);
|
||||
setDetalleDiarioCalculado([]);
|
||||
setPromediosPorDiaCalculado([]);
|
||||
setTotalesDetalle({ llevados: 0, devueltos: 0, ventaNeta: 0, promedioGeneralVentaNeta: 0, porcentajeDevolucionGeneral: 0 });
|
||||
setTotalesPromedios({ cantDias: 0, promLlevados: 0, promDevueltos: 0, promVentas: 0, porcentajeDevolucionGeneral: 0 });
|
||||
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 });
|
||||
setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0});
|
||||
|
||||
|
||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||
@@ -89,7 +89,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
const llevados = item.llevados || 0;
|
||||
const devueltos = item.devueltos || 0;
|
||||
const ventaNeta = llevados - devueltos;
|
||||
|
||||
|
||||
if (llevados > 0) {
|
||||
diasConActividadDetalle++;
|
||||
acumuladoVentaNeta += ventaNeta;
|
||||
@@ -101,7 +101,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
...item,
|
||||
id: `simple-can-${index}`, // o simple-dist-${index}
|
||||
ventaNeta: ventaNeta,
|
||||
promedio: promedioActual,
|
||||
promedio: promedioActual,
|
||||
porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0, // Esto es % Devolución real
|
||||
};
|
||||
});
|
||||
@@ -110,44 +110,45 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
const totalLlevadosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.llevados || 0), 0);
|
||||
const totalDevueltosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.devueltos || 0), 0);
|
||||
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
|
||||
|
||||
|
||||
setTotalesDetalle({
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: ultimoPromedioDetalle,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: ultimoPromedioDetalle,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
});
|
||||
|
||||
|
||||
// --- Cálculos para promedios y sus totales ---
|
||||
const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => {
|
||||
const promLlevados = item.promedio_Llevados || 0;
|
||||
const promDevueltos = item.promedio_Devueltos || 0;
|
||||
const promVentas = item.promedio_Ventas || 0;
|
||||
return {
|
||||
...item,
|
||||
id: `prom-can-${index}`,
|
||||
id: `prom-can-${index}`, // o prom-dist-${index}
|
||||
// LA COLUMNA EN EL PDF SE LLAMA "% Devolución" PERO PARECE SER "% VENTA"
|
||||
porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
|
||||
porcentajeDevolucion: promLlevados > 0 ? (promDevueltos / promLlevados) * 100 : 0,
|
||||
porcentajeDevolucion: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
|
||||
};
|
||||
});
|
||||
setPromediosPorDiaCalculado(promediosCalculadoLocal);
|
||||
|
||||
|
||||
const totalDiasProm = promediosCalculadoLocal.reduce((sum, item) => sum + (item.cant || 0), 0);
|
||||
const totalPonderadoLlevados = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
|
||||
const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0);
|
||||
// const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0); // No se usa para el % del PDF
|
||||
const totalPonderadoVentas = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
|
||||
|
||||
const promGeneralLlevados = totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0;
|
||||
const promGeneralDevueltos = totalDiasProm > 0 ? totalPonderadoDevueltos / totalDiasProm : 0;
|
||||
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasProm,
|
||||
promLlevados: promGeneralLlevados,
|
||||
promDevueltos: promGeneralDevueltos,
|
||||
promLlevados: totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0,
|
||||
promDevueltos: totalDiasProm > 0 ? promediosCalculadoLocal.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0) / promediosCalculadoLocal.length :0, // Promedio simple para mostrar
|
||||
promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0,
|
||||
porcentajeDevolucionGeneral: promGeneralLlevados > 0 ? (promGeneralDevueltos / promGeneralLlevados) * 100 : 0,
|
||||
// Para la fila "General" de promedios, el PDF usa (Total Prom. Ventas / Total Prom. Llevados) * 100
|
||||
// Usaremos los promedios generales calculados aquí
|
||||
porcentajeDevolucionGeneral: (totalDiasProm > 0 && (totalPonderadoLlevados / totalDiasProm) > 0)
|
||||
? ((totalPonderadoVentas / totalDiasProm) / (totalPonderadoLlevados / totalDiasProm)) * 100
|
||||
: 0,
|
||||
});
|
||||
|
||||
setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal });
|
||||
@@ -279,16 +280,16 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
</Box>
|
||||
{/* Contenedor para tus totales */}
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
|
||||
|
||||
// --- Custom Footer para Promedios por Día ---
|
||||
const CustomFooterPromedios = () => (
|
||||
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
@@ -296,11 +297,11 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
@@ -371,10 +372,10 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterPromedios }}
|
||||
hideFooterSelectedRowCount
|
||||
sx={{
|
||||
'& .MuiTablePagination-root': { // Oculta el paginador por defecto
|
||||
display: 'none',
|
||||
},
|
||||
sx={{
|
||||
'& .MuiTablePagination-root': { // Oculta el paginador por defecto
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
@@ -121,24 +121,20 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
porcentajeDevolucion: item.promedio_Llevados > 0 ? (item.promedio_Devueltos / item.promedio_Llevados) * 100 : 0,
|
||||
}));
|
||||
|
||||
// Calcular totales para la tabla de promedios (ponderados por Cant. Días)
|
||||
const totalDiasPromedios = promediosConCalculos.reduce((sum, item) => sum + (item.cant || 0), 0);
|
||||
const countPromedios = promediosConCalculos.length;
|
||||
const totalPonderadoLlevados = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
|
||||
const totalPonderadoDevueltos = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0);
|
||||
const totalPonderadoVentas = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
|
||||
|
||||
// LÓGICA DE PROMEDIO DE PROMEDIOS
|
||||
if (countPromedios > 0) {
|
||||
const sumPromLlevados = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Llevados || 0), 0);
|
||||
const sumPromDevueltos = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0);
|
||||
const sumPromVentas = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Ventas || 0), 0);
|
||||
const sumPorcDevolucion = promediosConCalculos.reduce((sum, item) => sum + (item.porcentajeDevolucion || 0), 0);
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasPromedios,
|
||||
promLlevados: totalDiasPromedios > 0 ? totalPonderadoLlevados / totalDiasPromedios : 0,
|
||||
promDevueltos: totalDiasPromedios > 0 ? totalPonderadoDevueltos / totalDiasPromedios : 0,
|
||||
promVentas: totalDiasPromedios > 0 ? totalPonderadoVentas / totalDiasPromedios : 0,
|
||||
porcentajeDevolucionGeneral: totalPonderadoLlevados > 0 ? (totalPonderadoDevueltos / totalPonderadoLlevados) * 100 : 0,
|
||||
});
|
||||
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasPromedios,
|
||||
promLlevados: sumPromLlevados / countPromedios,
|
||||
promDevueltos: sumPromDevueltos / countPromedios,
|
||||
promVentas: sumPromVentas / countPromedios,
|
||||
porcentajeDevolucionGeneral: sumPorcDevolucion / countPromedios,
|
||||
});
|
||||
}
|
||||
|
||||
setReportData({ detalleSimple: detalleConCalculos, promediosPorDia: promediosConCalculos });
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, Tooltip
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import EventAvailableIcon from '@mui/icons-material/EventAvailable';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
import cierresCcService from '../../services/Contables/cierresCcService';
|
||||
|
||||
interface SeleccionaReporteCuentasDistribuidoresProps {
|
||||
onGenerarReporte: (params: {
|
||||
@@ -22,18 +20,6 @@ interface SeleccionaReporteCuentasDistribuidoresProps {
|
||||
apiErrorMessage?: string | null;
|
||||
}
|
||||
|
||||
// Suma 1 día a una fecha en formato yyyy-MM-dd y devuelve la fecha resultante
|
||||
// también en yyyy-MM-dd. Usado por el atajo "Desde último cierre" para arrancar
|
||||
// el reporte el día siguiente al cierre.
|
||||
const addOneDay = (yyyyMmDd: string): string => {
|
||||
const d = new Date(yyyyMmDd + 'T00:00:00');
|
||||
d.setDate(d.getDate() + 1);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasDistribuidoresProps> = ({
|
||||
onGenerarReporte,
|
||||
isLoading,
|
||||
@@ -49,19 +35,15 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const [loadingUltimoCierre, setLoadingUltimoCierre] = useState(false);
|
||||
const [hayCierrePrevio, setHayCierrePrevio] = useState<boolean | null>(null);
|
||||
const [infoCierre, setInfoCierre] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidoresDropdown(),
|
||||
empresaService.getEmpresasDropdown()
|
||||
distribuidorService.getAllDistribuidoresDropdown(), // Asume que este servicio existe
|
||||
empresaService.getEmpresasDropdown() // Asume que este servicio existe
|
||||
]);
|
||||
setDistribuidores(distData.map(d => d));
|
||||
setDistribuidores(distData.map(d => d)); // El servicio devuelve tupla
|
||||
setEmpresas(empData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos:", error);
|
||||
@@ -73,13 +55,6 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Reset del estado del atajo cuando cambian distribuidor/empresa: el "ultimo cierre"
|
||||
// depende del par (distribuidor, empresa), si alguno cambia hay que volver a chequear.
|
||||
useEffect(() => {
|
||||
setHayCierrePrevio(null);
|
||||
setInfoCierre(null);
|
||||
}, [idDistribuidor, idEmpresa]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idDistribuidor) errors.idDistribuidor = 'Debe seleccionar un distribuidor.';
|
||||
@@ -103,37 +78,6 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
});
|
||||
};
|
||||
|
||||
const handleDesdeUltimoCierre = async () => {
|
||||
if (!idDistribuidor || !idEmpresa) return;
|
||||
setLoadingUltimoCierre(true);
|
||||
setInfoCierre(null);
|
||||
try {
|
||||
const ultimo = await cierresCcService.getUltimoCierre(Number(idDistribuidor), Number(idEmpresa));
|
||||
if (ultimo === null) {
|
||||
setHayCierrePrevio(false);
|
||||
setInfoCierre('Sin cierres previos para este distribuidor y empresa.');
|
||||
} else {
|
||||
const nuevaFechaDesde = addOneDay(ultimo.fechaCorte);
|
||||
setFechaDesde(nuevaFechaDesde);
|
||||
setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null }));
|
||||
setHayCierrePrevio(true);
|
||||
setInfoCierre(`Último cierre: ${ultimo.fechaCorte}. Fecha Desde ajustada al día siguiente.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error al obtener último cierre:', err);
|
||||
setInfoCierre('Error al consultar el último cierre.');
|
||||
} finally {
|
||||
setLoadingUltimoCierre(false);
|
||||
}
|
||||
};
|
||||
|
||||
const atajoDisabled = !idDistribuidor || !idEmpresa || loadingUltimoCierre || isLoading || hayCierrePrevio === false;
|
||||
const atajoTooltip = !idDistribuidor || !idEmpresa
|
||||
? 'Seleccioná distribuidor y empresa primero.'
|
||||
: hayCierrePrevio === false
|
||||
? 'Sin cierres previos.'
|
||||
: 'Autocompleta Fecha Desde con el día siguiente al último cierre.';
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
@@ -171,42 +115,19 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<TextField
|
||||
label="Fecha Desde"
|
||||
type="date"
|
||||
value={fechaDesde}
|
||||
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaDesde}
|
||||
helperText={localErrors.fechaDesde}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Tooltip title={atajoTooltip}>
|
||||
<span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={loadingUltimoCierre ? <CircularProgress size={16} /> : <EventAvailableIcon />}
|
||||
onClick={handleDesdeUltimoCierre}
|
||||
disabled={atajoDisabled}
|
||||
sx={{ mt: 2, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Desde último cierre
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{infoCierre && (
|
||||
<Alert severity={hayCierrePrevio ? 'success' : 'info'} sx={{ mt: 1 }}>
|
||||
{infoCierre}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Fecha Desde"
|
||||
type="date"
|
||||
value={fechaDesde}
|
||||
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaDesde}
|
||||
helperText={localErrors.fechaDesde}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Fecha Hasta"
|
||||
type="date"
|
||||
@@ -233,4 +154,4 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
);
|
||||
};
|
||||
|
||||
export default SeleccionaReporteCuentasDistribuidores;
|
||||
export default SeleccionaReporteCuentasDistribuidores;
|
||||
@@ -14,10 +14,6 @@ import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklis
|
||||
|
||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
|
||||
// Permisos exclusivos de SuperAdmin: no se asignan a perfiles ni se muestran en la UI de asignación.
|
||||
// CS002 = Ajuste manual de saldo. CC002 = Reapertura de cierres de cuenta corriente.
|
||||
const PERMISOS_SOLO_SUPERADMIN: ReadonlySet<string> = new Set(["CS002", "CC002"]);
|
||||
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS007") return "Suscripciones";
|
||||
@@ -48,8 +44,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos") ||
|
||||
moduloLower.includes("cuentas cierres")) {
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
return "Contables";
|
||||
}
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
@@ -109,12 +104,9 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
perfilService.getPerfilById(idPerfilNum),
|
||||
perfilService.getPermisosPorPerfil(idPerfilNum) // Esto devuelve todos los permisos con su estado 'asignado'
|
||||
]);
|
||||
// Filtrar permisos exclusivos de SuperAdmin (CS002, CC002) para que no aparezcan asignables.
|
||||
// No se altera la asignación existente en la DB — solo se ocultan de esta UI.
|
||||
const permisosVisibles = permisosData.filter(p => !PERMISOS_SOLO_SUPERADMIN.has(p.codAcc));
|
||||
setPerfil(perfilData);
|
||||
setPermisosDisponibles(permisosVisibles);
|
||||
setPermisosSeleccionados(new Set(permisosVisibles.filter(p => p.asignado).map(p => p.id)));
|
||||
setPermisosDisponibles(permisosData);
|
||||
setPermisosSeleccionados(new Set(permisosData.filter(p => p.asignado).map(p => p.id)));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Error al cargar datos del perfil o permisos.');
|
||||
|
||||
@@ -39,7 +39,6 @@ import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage';
|
||||
import GestionarPagosDistribuidorPage from '../pages/Contables/GestionarPagosDistribuidorPage';
|
||||
import GestionarNotasCDPage from '../pages/Contables/GestionarNotasCDPage';
|
||||
import GestionarSaldosPage from '../pages/Contables/GestionarSaldosPage';
|
||||
import CierresCuentaCorrientePage from '../pages/Contables/CierresCuentaCorrientePage';
|
||||
|
||||
// Usuarios
|
||||
import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente
|
||||
@@ -242,14 +241,6 @@ const AppRoutes = () => {
|
||||
<Route path="pagos-distribuidores" element={<GestionarPagosDistribuidorPage />} />
|
||||
<Route path="notas-cd" element={<GestionarNotasCDPage />} />
|
||||
<Route path="gestion-saldos" element={<GestionarSaldosPage />} />
|
||||
<Route
|
||||
path="cierres-cc"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="CC003" sectionName="Cierres de Cuenta Corriente">
|
||||
<CierresCuentaCorrientePage />
|
||||
</SectionProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Impresión (anidado) */}
|
||||
|
||||
@@ -29,7 +29,6 @@ import type { PerfilHistorialDto } from '../../models/dtos/Auditoria/PerfilHisto
|
||||
import type { PermisoHistorialDto } from '../../models/dtos/Auditoria/PermisoHistorialDto';
|
||||
import type { PermisosPerfilesHistorialDto } from '../../models/dtos/Auditoria/PermisosPerfilesHistorialDto';
|
||||
import type { CambioParadaHistorialDto } from '../../models/dtos/Auditoria/CambioParadaHistorialDto';
|
||||
import type { CierreCuentaCorrienteHistorialDto } from '../../models/dtos/Auditoria/CierreCuentaCorrienteHistorialDto';
|
||||
|
||||
interface HistorialParamsComunes {
|
||||
fechaDesde?: string; // "yyyy-MM-dd"
|
||||
@@ -42,10 +41,6 @@ interface HistorialCambiosParadaParams extends HistorialParamsComunes {
|
||||
idCanillaAfectado?: number;
|
||||
}
|
||||
|
||||
interface HistorialCierresCCParams extends HistorialParamsComunes {
|
||||
idCierreAfectado?: number;
|
||||
}
|
||||
|
||||
interface HistorialPermisosPerfilesParams extends HistorialParamsComunes {
|
||||
idPerfilAfectado?: number;
|
||||
idPermisoAfectado?: number;
|
||||
@@ -467,20 +462,11 @@ const getHistorialCambiosParada = async (params: HistorialCambiosParadaParams):
|
||||
const queryParams: any = { ...params };
|
||||
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
|
||||
delete queryParams.idUsuarioModificador;
|
||||
|
||||
|
||||
const response = await apiClient.get<CambioParadaHistorialDto[]>('/auditoria/cambios-parada-canilla', { params: queryParams });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getHistorialCierresCC = async (params: HistorialCierresCCParams): Promise<CierreCuentaCorrienteHistorialDto[]> => {
|
||||
const queryParams: any = { ...params };
|
||||
if (params.idUsuarioModificador) queryParams.idUsuarioModifico = params.idUsuarioModificador;
|
||||
delete queryParams.idUsuarioModificador;
|
||||
|
||||
const response = await apiClient.get<CierreCuentaCorrienteHistorialDto[]>('/auditoria/cierres-cuenta-corriente', { params: queryParams });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const auditoriaService = {
|
||||
getHistorialUsuarios,
|
||||
getHistorialPagosDistribuidor,
|
||||
@@ -512,7 +498,6 @@ const auditoriaService = {
|
||||
getHistorialPermisosMaestro,
|
||||
getHistorialPermisosPerfiles,
|
||||
getHistorialCambiosParada,
|
||||
getHistorialCierresCC,
|
||||
};
|
||||
|
||||
export default auditoriaService;
|
||||
@@ -1,75 +0,0 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { CierreCuentaCorrienteDto } from '../../models/dtos/Contables/CierreCuentaCorrienteDto';
|
||||
import type { CrearCierreDto } from '../../models/dtos/Contables/CrearCierreDto';
|
||||
import type { ReabrirCierreDto } from '../../models/dtos/Contables/ReabrirCierreDto';
|
||||
import type { UltimoCierreDto } from '../../models/dtos/Contables/UltimoCierreDto';
|
||||
import type { CierreCuentaCorrienteHistorialDto } from '../../models/dtos/Auditoria/CierreCuentaCorrienteHistorialDto';
|
||||
|
||||
const getAllCierres = async (
|
||||
idDistribuidor: number,
|
||||
idEmpresa: number,
|
||||
): Promise<CierreCuentaCorrienteDto[]> => {
|
||||
const response = await apiClient.get<CierreCuentaCorrienteDto[]>('/cierres-cc', {
|
||||
params: { idDistribuidor, idEmpresa },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Devuelve null si no hay cierre vigente (backend responde 404 — lo capturamos acá
|
||||
// para que las páginas no tengan que manejar el error).
|
||||
const getUltimoCierre = async (
|
||||
idDistribuidor: number,
|
||||
idEmpresa: number,
|
||||
): Promise<UltimoCierreDto | null> => {
|
||||
try {
|
||||
const response = await apiClient.get<UltimoCierreDto>('/cierres-cc/ultimo', {
|
||||
params: { idDistribuidor, idEmpresa },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'response' in error &&
|
||||
(error as { response?: { status?: number } }).response?.status === 404
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const crearCierre = async (data: CrearCierreDto): Promise<CierreCuentaCorrienteDto> => {
|
||||
const response = await apiClient.post<CierreCuentaCorrienteDto>('/cierres-cc', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const reabrirCierre = async (
|
||||
idCierre: number,
|
||||
data: ReabrirCierreDto,
|
||||
): Promise<CierreCuentaCorrienteDto> => {
|
||||
const response = await apiClient.post<CierreCuentaCorrienteDto>(
|
||||
`/cierres-cc/${idCierre}/reabrir`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getHistorialCierre = async (
|
||||
idCierre: number,
|
||||
): Promise<CierreCuentaCorrienteHistorialDto[]> => {
|
||||
const response = await apiClient.get<CierreCuentaCorrienteHistorialDto[]>(
|
||||
`/cierres-cc/${idCierre}/historial`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const cierresCcService = {
|
||||
getAllCierres,
|
||||
getUltimoCierre,
|
||||
crearCierre,
|
||||
reabrirCierre,
|
||||
getHistorialCierre,
|
||||
};
|
||||
|
||||
export default cierresCcService;
|
||||
@@ -5,10 +5,8 @@ import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/Updat
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import type { DistribuidorLookupDto } from '../../models/dtos/Distribucion/DistribuidorLookupDto';
|
||||
|
||||
const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string, soloActivos: boolean = true): Promise<DistribuidorDto[]> => {
|
||||
const params: Record<string, string | boolean> = {
|
||||
soloActivos: soloActivos
|
||||
};
|
||||
const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string): Promise<DistribuidorDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
if (nombreFilter) params.nombre = nombreFilter;
|
||||
if (nroDocFilter) params.nroDoc = nroDocFilter;
|
||||
|
||||
@@ -39,15 +37,11 @@ const deleteDistribuidor = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/distribuidores/${id}`);
|
||||
};
|
||||
|
||||
const getAllDistribuidoresDropdown = async (soloActivos: boolean = true): Promise<DistribuidorDropdownDto[]> => {
|
||||
const response = await apiClient.get<DistribuidorDropdownDto[]>('/distribuidores/dropdown', { params: { soloActivos } });
|
||||
const getAllDistribuidoresDropdown = async (): Promise<DistribuidorDropdownDto[]> => {
|
||||
const response = await apiClient.get<DistribuidorDropdownDto[]>('/distribuidores/dropdown');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const toggleBajaDistribuidor = async (id: number, data: { darDeBaja: boolean, fechaBaja: string | null }): Promise<void> => {
|
||||
await apiClient.put(`/distribuidores/${id}/toggle-baja`, data);
|
||||
};
|
||||
|
||||
const distribuidorService = {
|
||||
getAllDistribuidores,
|
||||
getDistribuidorById,
|
||||
@@ -56,7 +50,6 @@ const distribuidorService = {
|
||||
deleteDistribuidor,
|
||||
getAllDistribuidoresDropdown,
|
||||
getDistribuidorLookupById,
|
||||
toggleBajaDistribuidor,
|
||||
};
|
||||
|
||||
export default distribuidorService;
|
||||
@@ -3,8 +3,6 @@ import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'
|
||||
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
|
||||
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
|
||||
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||
import type { CreateStockBobinaLoteDto } from '../../models/dtos/Impresion/CreateStockBobinaLoteDto';
|
||||
import type { UpdateFechaRemitoLoteDto } from '../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||
|
||||
interface GetAllStockBobinasParams {
|
||||
idTipoBobina?: number | null;
|
||||
@@ -14,8 +12,6 @@ interface GetAllStockBobinasParams {
|
||||
remitoFilter?: string | null;
|
||||
fechaDesde?: string | null; // "yyyy-MM-dd"
|
||||
fechaHasta?: string | null; // "yyyy-MM-dd"
|
||||
fechaEstadoDesde?: string | null; // "yyyy-MM-dd"
|
||||
fechaEstadoHasta?: string | null; // "yyyy-MM-dd"
|
||||
}
|
||||
|
||||
const getAllStockBobinas = async (filters: GetAllStockBobinasParams): Promise<StockBobinaDto[]> => {
|
||||
@@ -27,8 +23,6 @@ const getAllStockBobinas = async (filters: GetAllStockBobinasParams): Promise<St
|
||||
if (filters.remitoFilter) params.remito = filters.remitoFilter; // El backend espera remito
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.fechaEstadoDesde) params.fechaEstadoDesde = filters.fechaEstadoDesde;
|
||||
if (filters.fechaEstadoHasta) params.fechaEstadoHasta = filters.fechaEstadoHasta;
|
||||
|
||||
const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas', { params });
|
||||
return response.data;
|
||||
@@ -56,23 +50,6 @@ const deleteIngresoBobina = async (idBobina: number): Promise<void> => {
|
||||
await apiClient.delete(`/stockbobinas/${idBobina}`);
|
||||
};
|
||||
|
||||
const verificarRemitoExistente = async (idPlanta: number, remito: string, fechaRemito?: string | null): Promise<StockBobinaDto[]> => {
|
||||
const params: { idPlanta: number; remito: string; fechaRemito?: string } = { idPlanta, remito };
|
||||
if (fechaRemito) {
|
||||
params.fechaRemito = fechaRemito;
|
||||
}
|
||||
const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas/verificar-remito', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const ingresarLoteBobinas = async (data: CreateStockBobinaLoteDto): Promise<void> => {
|
||||
await apiClient.post('/stockbobinas/lote', data);
|
||||
};
|
||||
|
||||
const actualizarFechaRemitoLote = async (data: UpdateFechaRemitoLoteDto): Promise<void> => {
|
||||
await apiClient.put('/stockbobinas/actualizar-fecha-remito', data);
|
||||
};
|
||||
|
||||
const stockBobinaService = {
|
||||
getAllStockBobinas,
|
||||
getStockBobinaById,
|
||||
@@ -80,9 +57,6 @@ const stockBobinaService = {
|
||||
updateDatosBobinaDisponible,
|
||||
cambiarEstadoBobina,
|
||||
deleteIngresoBobina,
|
||||
verificarRemitoExistente,
|
||||
ingresarLoteBobinas,
|
||||
actualizarFechaRemitoLote,
|
||||
};
|
||||
|
||||
export default stockBobinaService;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user