Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using GestionIntegral.Api.Services.Contables;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Contables
|
||||
{
|
||||
[Route("api/saldos")]
|
||||
[ApiController]
|
||||
[Authorize] // Requiere autenticación para todos los endpoints
|
||||
public class SaldosController : ControllerBase
|
||||
{
|
||||
private readonly ISaldoService _saldoService;
|
||||
private readonly ILogger<SaldosController> _logger;
|
||||
|
||||
// Define un permiso específico para ver saldos, y otro para ajustarlos (SuperAdmin implícito)
|
||||
private const string PermisoVerSaldos = "CS001"; // Ejemplo: Cuentas Saldos Ver
|
||||
private const string PermisoAjustarSaldos = "CS002"; // Ejemplo: Cuentas Saldos Ajustar (o solo SuperAdmin)
|
||||
|
||||
|
||||
public SaldosController(ISaldoService saldoService, ILogger<SaldosController> logger)
|
||||
{
|
||||
_saldoService = saldoService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private bool TienePermiso(string codAccRequerido)
|
||||
{
|
||||
if (User.IsInRole("SuperAdmin")) return true;
|
||||
return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido);
|
||||
}
|
||||
private int? GetCurrentUserId()
|
||||
{
|
||||
if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId;
|
||||
_logger.LogWarning("No se pudo obtener el UserId del token JWT en SaldosController.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET: api/saldos
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<SaldoGestionDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetSaldosGestion(
|
||||
[FromQuery] string? destino,
|
||||
[FromQuery] int? idDestino,
|
||||
[FromQuery] int? idEmpresa)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerSaldos)) // Usar el nuevo permiso
|
||||
{
|
||||
_logger.LogWarning("Acceso denegado a GetSaldosGestion para Usuario ID {userId}", GetCurrentUserId() ?? 0);
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var saldos = await _saldoService.ObtenerSaldosParaGestionAsync(destino, idDestino, idEmpresa);
|
||||
return Ok(saldos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener saldos para gestión.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener saldos.");
|
||||
}
|
||||
}
|
||||
|
||||
// POST: api/saldos/ajustar
|
||||
[HttpPost("ajustar")]
|
||||
[ProducesResponseType(typeof(SaldoGestionDto), StatusCodes.Status200OK)] // Devuelve el saldo actualizado
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)] // Solo SuperAdmin o con permiso específico
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> AjustarSaldoManualmente([FromBody] AjusteSaldoRequestDto ajusteDto)
|
||||
{
|
||||
// Esta operación debería ser MUY restringida. Solo SuperAdmin o un permiso muy específico.
|
||||
if (!User.IsInRole("SuperAdmin") && !TienePermiso(PermisoAjustarSaldos))
|
||||
{
|
||||
_logger.LogWarning("Intento no autorizado de ajustar saldo por Usuario ID {userId}", GetCurrentUserId() ?? 0);
|
||||
return Forbid("No tiene permisos para realizar ajustes manuales de saldo.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var idUsuario = GetCurrentUserId();
|
||||
if (idUsuario == null) return Unauthorized("No se pudo identificar al usuario.");
|
||||
|
||||
try
|
||||
{
|
||||
var (exito, error, saldoActualizado) = await _saldoService.RealizarAjusteManualSaldoAsync(ajusteDto, idUsuario.Value);
|
||||
if (!exito)
|
||||
{
|
||||
if (error != null && error.Contains("No se encontró un saldo existente"))
|
||||
return NotFound(new { message = error });
|
||||
return BadRequest(new { message = error ?? "Error desconocido al ajustar el saldo." });
|
||||
}
|
||||
return Ok(saldoActualizado);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error crítico al ajustar saldo manualmente.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar el ajuste de saldo.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,11 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<CanillaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? soloActivos = true)
|
||||
public async Task<IActionResult> GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||
var canillas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos);
|
||||
return Ok(canillas);
|
||||
var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); // <<-- Pasa el parámetro
|
||||
return Ok(canillitas);
|
||||
}
|
||||
|
||||
// GET: api/canillas/{id}
|
||||
@@ -117,7 +117,7 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
public async Task<IActionResult> ToggleBajaCanilla(int id, [FromBody] ToggleBajaCanillaDto bajaDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoBaja)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var idUsuario = GetCurrentUserId();
|
||||
if (idUsuario == null) return Unauthorized();
|
||||
|
||||
|
||||
@@ -47,6 +47,15 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
return Ok(distribuidores);
|
||||
}
|
||||
|
||||
[HttpGet("dropdown")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAllDropdownDistribuidores()
|
||||
{
|
||||
var distribuidores = await _distribuidorService.GetAllDropdownAsync();
|
||||
return Ok(distribuidores);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}", Name = "GetDistribuidorById")]
|
||||
[ProducesResponseType(typeof(DistribuidorDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
@@ -59,6 +68,17 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
return Ok(distribuidor);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/lookup", Name = "GetDistribuidorLookupById")]
|
||||
[ProducesResponseType(typeof(DistribuidorLookupDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ObtenerLookupPorIdAsync(int id)
|
||||
{
|
||||
var distribuidor = await _distribuidorService.ObtenerLookupPorIdAsync(id);
|
||||
if (distribuidor == null) return NotFound();
|
||||
return Ok(distribuidor);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(DistribuidorDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -74,6 +74,25 @@ namespace GestionIntegral.Api.Controllers // Ajusta el namespace si es necesario
|
||||
}
|
||||
}
|
||||
|
||||
// GET: api/empresas/dropdown
|
||||
[HttpGet("dropdown")]
|
||||
[ProducesResponseType(typeof(IEnumerable<EmpresaDropdownDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetEmpresasDropdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
var empresas = await _empresaService.ObtenerParaDropdown();
|
||||
return Ok(empresas);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener todas las Empresas.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener las empresas.");
|
||||
}
|
||||
}
|
||||
|
||||
// GET: api/empresas/{id}
|
||||
// Permiso Requerido: DE001 (Ver Empresas)
|
||||
[HttpGet("{id:int}", Name = "GetEmpresaById")]
|
||||
@@ -101,6 +120,29 @@ namespace GestionIntegral.Api.Controllers // Ajusta el namespace si es necesario
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/lookup", Name = "GetEmpresaLookupById")]
|
||||
[ProducesResponseType(typeof(EmpresaDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ObtenerLookupPorIdAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var empresa = await _empresaService.ObtenerLookupPorIdAsync(id);
|
||||
if (empresa == null)
|
||||
{
|
||||
return NotFound(new { message = $"Empresa con ID {id} no encontrada." });
|
||||
}
|
||||
return Ok(empresa);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener Empresa por ID: {Id}", id);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la empresa.");
|
||||
}
|
||||
}
|
||||
|
||||
// POST: api/empresas
|
||||
// Permiso Requerido: DE002 (Agregar Empresas)
|
||||
[HttpPost]
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Services.Distribucion;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
{
|
||||
[Route("api/novedadescanilla")] // Ruta base más genérica para las novedades
|
||||
[ApiController]
|
||||
[Authorize] // Todas las acciones requieren autenticación
|
||||
public class NovedadesCanillaController : ControllerBase
|
||||
{
|
||||
private readonly INovedadCanillaService _novedadService;
|
||||
private readonly ILogger<NovedadesCanillaController> _logger;
|
||||
|
||||
public NovedadesCanillaController(INovedadCanillaService novedadService, ILogger<NovedadesCanillaController> logger)
|
||||
{
|
||||
_novedadService = novedadService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// --- Helper para verificar permisos ---
|
||||
private bool TienePermiso(string codAccRequerido)
|
||||
{
|
||||
if (User.IsInRole("SuperAdmin")) return true;
|
||||
return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido);
|
||||
}
|
||||
|
||||
// --- Helper para obtener User ID ---
|
||||
private int? GetCurrentUserId()
|
||||
{
|
||||
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
|
||||
if (int.TryParse(userIdClaim, out int userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
_logger.LogWarning("No se pudo obtener el UserId del token JWT en NovedadesCanillaController.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET: api/novedadescanilla/porcanilla/{idCanilla}
|
||||
// Obtiene todas las novedades para un canillita específico, opcionalmente filtrado por fecha.
|
||||
// Permiso: CG001 (Ver Canillas) o CG006 (Gestionar Novedades).
|
||||
// Si CG006 es "Permite la Carga/Modificación", entonces CG001 podría ser más apropiado solo para ver.
|
||||
// Vamos a usar CG001 para ver. Si se quiere más granularidad, se puede crear un permiso "Ver Novedades".
|
||||
[HttpGet("porcanilla/{idCanilla:int}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<NovedadCanillaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetNovedadesPorCanilla(int idCanilla, [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta)
|
||||
{
|
||||
if (!TienePermiso("CG001") && !TienePermiso("CG006")) // Necesita al menos uno de los dos
|
||||
{
|
||||
_logger.LogWarning("Acceso denegado a GetNovedadesPorCanilla para el usuario {UserId} y canillita {IdCanilla}", GetCurrentUserId() ?? 0, idCanilla);
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var novedades = await _novedadService.ObtenerPorCanillaAsync(idCanilla, fechaDesde, fechaHasta);
|
||||
return Ok(novedades);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener novedades para Canillita ID: {IdCanilla}", idCanilla);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener las novedades.");
|
||||
}
|
||||
}
|
||||
|
||||
// GET: api/novedadescanilla/{idNovedad}
|
||||
// Obtiene una novedad específica por su ID.
|
||||
// Permiso: CG001 o CG006
|
||||
[HttpGet("{idNovedad:int}", Name = "GetNovedadCanillaById")]
|
||||
[ProducesResponseType(typeof(NovedadCanillaDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetNovedadCanillaById(int idNovedad)
|
||||
{
|
||||
if (!TienePermiso("CG001") && !TienePermiso("CG006")) return Forbid();
|
||||
|
||||
try
|
||||
{
|
||||
var novedad = await _novedadService.ObtenerPorIdAsync(idNovedad);
|
||||
if (novedad == null)
|
||||
{
|
||||
return NotFound(new { message = $"Novedad con ID {idNovedad} no encontrada." });
|
||||
}
|
||||
return Ok(novedad);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener NovedadCanilla por ID: {IdNovedad}", idNovedad);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la novedad.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// POST: api/novedadescanilla
|
||||
// Crea una nueva novedad. El IdCanilla viene en el DTO.
|
||||
// Permiso: CG006 (Permite la Carga/Modificación de Novedades)
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(NovedadCanillaDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CreateNovedadCanilla([FromBody] CreateNovedadCanillaDto createDto)
|
||||
{
|
||||
if (!TienePermiso("CG006")) return Forbid();
|
||||
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var idUsuario = GetCurrentUserId();
|
||||
if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token.");
|
||||
|
||||
try
|
||||
{
|
||||
var (novedadCreada, error) = await _novedadService.CrearAsync(createDto, idUsuario.Value);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (novedadCreada == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la novedad.");
|
||||
|
||||
// Devuelve la ruta al recurso creado y el recurso mismo
|
||||
return CreatedAtRoute("GetNovedadCanillaById", new { idNovedad = novedadCreada.IdNovedad }, novedadCreada);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al crear NovedadCanilla para Canilla ID: {IdCanilla} por Usuario ID: {UsuarioId}", createDto.IdCanilla, idUsuario);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al crear la novedad.");
|
||||
}
|
||||
}
|
||||
|
||||
// PUT: api/novedadescanilla/{idNovedad}
|
||||
// Actualiza una novedad existente.
|
||||
// Permiso: CG006
|
||||
[HttpPut("{idNovedad:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> UpdateNovedadCanilla(int idNovedad, [FromBody] UpdateNovedadCanillaDto updateDto)
|
||||
{
|
||||
if (!TienePermiso("CG006")) return Forbid();
|
||||
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var idUsuario = GetCurrentUserId();
|
||||
if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token.");
|
||||
|
||||
try
|
||||
{
|
||||
var (exito, error) = await _novedadService.ActualizarAsync(idNovedad, updateDto, idUsuario.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
if (error == "Novedad no encontrada.") return NotFound(new { message = error });
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al actualizar NovedadCanilla ID: {IdNovedad} por Usuario ID: {UsuarioId}", idNovedad, idUsuario);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar la novedad.");
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: api/novedadescanilla/{idNovedad}
|
||||
// Elimina una novedad.
|
||||
// Permiso: CG006 (Asumiendo que el mismo permiso para Carga/Modificación incluye eliminación)
|
||||
// Si la eliminación es un permiso separado (ej: CG00X), ajústalo.
|
||||
[HttpDelete("{idNovedad:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> DeleteNovedadCanilla(int idNovedad)
|
||||
{
|
||||
if (!TienePermiso("CG006")) return Forbid();
|
||||
|
||||
var idUsuario = GetCurrentUserId();
|
||||
if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token.");
|
||||
|
||||
try
|
||||
{
|
||||
var (exito, error) = await _novedadService.EliminarAsync(idNovedad, idUsuario.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
if (error == "Novedad no encontrada.") return NotFound(new { message = error });
|
||||
return BadRequest(new { message = error }); // Podría ser otro error, como "no se pudo eliminar"
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al eliminar NovedadCanilla ID: {IdNovedad} por Usuario ID: {UsuarioId}", idNovedad, idUsuario);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al eliminar la novedad.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using GestionIntegral.Api.Data.Repositories.Impresion;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Services.Distribucion;
|
||||
|
||||
namespace GestionIntegral.Api.Controllers
|
||||
{
|
||||
@@ -25,6 +26,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
private readonly IPublicacionRepository _publicacionRepository;
|
||||
private readonly IEmpresaRepository _empresaRepository;
|
||||
private readonly IDistribuidorRepository _distribuidorRepository; // Para obtener el nombre del distribuidor
|
||||
private readonly INovedadCanillaService _novedadCanillaService;
|
||||
|
||||
|
||||
// Permisos
|
||||
@@ -36,22 +38,25 @@ namespace GestionIntegral.Api.Controllers
|
||||
private const string PermisoVerBalanceCuentas = "RR001";
|
||||
private const string PermisoVerReporteTiradas = "RR008";
|
||||
private const string PermisoVerReporteConsumoBobinas = "RR007";
|
||||
|
||||
private const string PermisoVerReporteNovedadesCanillas = "RR004";
|
||||
private const string PermisoVerReporteListadoDistMensual = "RR009";
|
||||
|
||||
public ReportesController(
|
||||
IReportesService reportesService, // <--- CORREGIDO
|
||||
IReportesService reportesService,
|
||||
INovedadCanillaService novedadCanillaService,
|
||||
ILogger<ReportesController> logger,
|
||||
IPlantaRepository plantaRepository,
|
||||
IPublicacionRepository publicacionRepository,
|
||||
IEmpresaRepository empresaRepository,
|
||||
IDistribuidorRepository distribuidorRepository) // Añadido
|
||||
IDistribuidorRepository distribuidorRepository)
|
||||
{
|
||||
_reportesService = reportesService; // <--- CORREGIDO
|
||||
_reportesService = reportesService;
|
||||
_novedadCanillaService = novedadCanillaService;
|
||||
_logger = logger;
|
||||
_plantaRepository = plantaRepository;
|
||||
_publicacionRepository = publicacionRepository;
|
||||
_empresaRepository = empresaRepository;
|
||||
_distribuidorRepository = distribuidorRepository; // Añadido
|
||||
_distribuidorRepository = distribuidorRepository;
|
||||
}
|
||||
|
||||
private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc);
|
||||
@@ -1457,5 +1462,277 @@ namespace GestionIntegral.Api.Controllers
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al generar el PDF del ticket.");
|
||||
}
|
||||
}
|
||||
|
||||
// GET: api/reportes/novedades-canillas
|
||||
// Obtiene los datos para el reporte de novedades de canillitas
|
||||
[HttpGet("novedades-canillas")]
|
||||
[ProducesResponseType(typeof(IEnumerable<NovedadesCanillasReporteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)] // Si no hay datos
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetReporteNovedadesCanillasData(
|
||||
[FromQuery] int idEmpresa,
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteNovedadesCanillas))
|
||||
{
|
||||
_logger.LogWarning("Acceso denegado a GetReporteNovedadesCanillasData. Usuario: {User}", User.Identity?.Name ?? "Desconocido");
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
if (fechaDesde > fechaHasta)
|
||||
{
|
||||
return BadRequest(new { message = "La fecha 'desde' no puede ser posterior a la fecha 'hasta'." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reporteData = await _novedadCanillaService.ObtenerReporteNovedadesAsync(idEmpresa, fechaDesde, fechaHasta);
|
||||
if (reporteData == null || !reporteData.Any())
|
||||
{
|
||||
// Devolver Ok con array vacío en lugar de NotFound para que el frontend pueda manejarlo como "sin datos"
|
||||
return Ok(Enumerable.Empty<NovedadesCanillasReporteDto>());
|
||||
}
|
||||
return Ok(reporteData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al generar datos para el reporte de novedades de canillitas. Empresa: {IdEmpresa}, Desde: {FechaDesde}, Hasta: {FechaHasta}", idEmpresa, fechaDesde, fechaHasta);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Error interno al generar el reporte de novedades." });
|
||||
}
|
||||
}
|
||||
|
||||
// GET: api/reportes/novedades-canillas/pdf
|
||||
// Genera el PDF del reporte de novedades de canillitas
|
||||
[HttpGet("novedades-canillas/pdf")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetReporteNovedadesCanillasPdf(
|
||||
[FromQuery] int idEmpresa,
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteNovedadesCanillas)) // RR004
|
||||
{
|
||||
_logger.LogWarning("Acceso denegado a GetReporteNovedadesCanillasPdf. Usuario: {User}", User.Identity?.Name ?? "Desconocido");
|
||||
return Forbid();
|
||||
}
|
||||
if (fechaDesde > fechaHasta)
|
||||
{
|
||||
return BadRequest(new { message = "La fecha 'desde' no puede ser posterior a la fecha 'hasta'." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Obtener datos para AMBOS datasets
|
||||
var novedadesData = await _novedadCanillaService.ObtenerReporteNovedadesAsync(idEmpresa, fechaDesde, fechaHasta);
|
||||
var gananciasData = await _novedadCanillaService.ObtenerReporteGananciasAsync(idEmpresa, fechaDesde, fechaHasta); // << OBTENER DATOS DE GANANCIAS
|
||||
|
||||
// Verificar si hay datos en *alguno* de los datasets necesarios para el reporte
|
||||
if ((novedadesData == null || !novedadesData.Any()) && (gananciasData == null || !gananciasData.Any()))
|
||||
{
|
||||
return NotFound(new { message = "No hay datos para generar el PDF con los parámetros seleccionados." });
|
||||
}
|
||||
|
||||
var empresa = await _empresaRepository.GetByIdAsync(idEmpresa);
|
||||
|
||||
LocalReport report = new LocalReport();
|
||||
string rdlcPath = Path.Combine("Controllers", "Reportes", "RDLC", "ReporteListadoNovedadesCanillas.rdlc");
|
||||
if (!System.IO.File.Exists(rdlcPath))
|
||||
{
|
||||
_logger.LogError("Archivo RDLC no encontrado en la ruta: {RdlcPath}", rdlcPath);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Archivo de definición de reporte no encontrado.");
|
||||
}
|
||||
|
||||
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
report.LoadReportDefinition(fs);
|
||||
}
|
||||
|
||||
// Nombre del DataSet en RDLC para SP_DistCanillasNovedades (detalles)
|
||||
report.DataSources.Add(new ReportDataSource("DSNovedadesCanillasDetalles", novedadesData ?? new List<NovedadesCanillasReporteDto>()));
|
||||
|
||||
// Nombre del DataSet en RDLC para SP_DistCanillasGanancias (ganancias/resumen)
|
||||
report.DataSources.Add(new ReportDataSource("DSNovedadesCanillas", gananciasData ?? new List<CanillaGananciaReporteDto>()));
|
||||
|
||||
|
||||
var parameters = new List<ReportParameter>
|
||||
{
|
||||
new ReportParameter("NomEmp", empresa?.Nombre ?? "N/A"),
|
||||
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
|
||||
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy"))
|
||||
};
|
||||
report.SetParameters(parameters);
|
||||
|
||||
byte[] pdfBytes = report.Render("PDF");
|
||||
string fileName = $"ReporteNovedadesCanillas_Emp{idEmpresa}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf";
|
||||
return File(pdfBytes, "application/pdf", fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al generar PDF para el reporte de novedades de canillitas. Empresa: {IdEmpresa}", idEmpresa);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = $"Error interno al generar el PDF: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
// GET: api/reportes/novedades-canillas-ganancias
|
||||
[HttpGet("novedades-canillas-ganancias")]
|
||||
[ProducesResponseType(typeof(IEnumerable<CanillaGananciaReporteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetReporteGananciasCanillasData(
|
||||
[FromQuery] int idEmpresa,
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteNovedadesCanillas)) return Forbid(); // RR004
|
||||
|
||||
if (fechaDesde > fechaHasta)
|
||||
{
|
||||
return BadRequest(new { message = "La fecha 'desde' no puede ser posterior a la fecha 'hasta'." });
|
||||
}
|
||||
try
|
||||
{
|
||||
var gananciasData = await _novedadCanillaService.ObtenerReporteGananciasAsync(idEmpresa, fechaDesde, fechaHasta);
|
||||
if (gananciasData == null || !gananciasData.Any())
|
||||
{
|
||||
return Ok(Enumerable.Empty<CanillaGananciaReporteDto>());
|
||||
}
|
||||
return Ok(gananciasData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener datos de ganancias para el reporte de novedades. Empresa: {IdEmpresa}", idEmpresa);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Error interno al obtener datos de ganancias." });
|
||||
}
|
||||
}
|
||||
|
||||
// GET: api/reportes/listado-distribucion-mensual/diarios
|
||||
[HttpGet("listado-distribucion-mensual/diarios")]
|
||||
[ProducesResponseType(typeof(IEnumerable<ListadoDistCanMensualDiariosDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetListadoDistMensualDiarios(
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta,
|
||||
[FromQuery] bool esAccionista)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid();
|
||||
if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." });
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerReporteMensualDiariosAsync(fechaDesde, fechaHasta, esAccionista);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
return Ok(data ?? Enumerable.Empty<ListadoDistCanMensualDiariosDto>());
|
||||
}
|
||||
|
||||
[HttpGet("listado-distribucion-mensual/diarios/pdf")]
|
||||
public async Task<IActionResult> GetListadoDistMensualDiariosPdf(
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta,
|
||||
[FromQuery] bool esAccionista)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid();
|
||||
if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." });
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerReporteMensualDiariosAsync(fechaDesde, fechaHasta, esAccionista);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el PDF." });
|
||||
|
||||
try
|
||||
{
|
||||
LocalReport report = new LocalReport();
|
||||
string rdlcPath = Path.Combine("Controllers", "Reportes", "RDLC", "ReporteListadoDistribucionCanMensualDiarios.rdlc");
|
||||
if (!System.IO.File.Exists(rdlcPath))
|
||||
{
|
||||
_logger.LogError("Archivo RDLC no encontrado: {Path}", rdlcPath);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"Archivo de reporte no encontrado: {Path.GetFileName(rdlcPath)}");
|
||||
}
|
||||
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
report.LoadReportDefinition(fs);
|
||||
}
|
||||
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCanMensualDiarios", data));
|
||||
|
||||
var parameters = new List<ReportParameter>
|
||||
{
|
||||
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
|
||||
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")),
|
||||
new ReportParameter("CanAcc", esAccionista ? "1" : "0") // El RDLC espera un Integer para CanAcc
|
||||
};
|
||||
report.SetParameters(parameters);
|
||||
|
||||
byte[] pdfBytes = report.Render("PDF");
|
||||
string tipoDesc = esAccionista ? "Accionistas" : "Canillitas";
|
||||
return File(pdfBytes, "application/pdf", $"ListadoDistMensualDiarios_{tipoDesc}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf");
|
||||
}
|
||||
catch (Exception ex) { _logger.LogError(ex, "Error PDF ListadoDistMensualDiarios"); return StatusCode(500, "Error interno."); }
|
||||
}
|
||||
|
||||
|
||||
// GET: api/reportes/listado-distribucion-mensual/publicaciones
|
||||
[HttpGet("listado-distribucion-mensual/publicaciones")]
|
||||
[ProducesResponseType(typeof(IEnumerable<ListadoDistCanMensualPubDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetListadoDistMensualPorPublicacion(
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta,
|
||||
[FromQuery] bool esAccionista)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid();
|
||||
if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." });
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerReporteMensualPorPublicacionAsync(fechaDesde, fechaHasta, esAccionista);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
return Ok(data ?? Enumerable.Empty<ListadoDistCanMensualPubDto>());
|
||||
}
|
||||
|
||||
[HttpGet("listado-distribucion-mensual/publicaciones/pdf")]
|
||||
public async Task<IActionResult> GetListadoDistMensualPorPublicacionPdf(
|
||||
[FromQuery] DateTime fechaDesde,
|
||||
[FromQuery] DateTime fechaHasta,
|
||||
[FromQuery] bool esAccionista)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerReporteListadoDistMensual)) return Forbid();
|
||||
if (fechaDesde > fechaHasta) return BadRequest(new { message = "Fecha Desde no puede ser mayor a Fecha Hasta." });
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerReporteMensualPorPublicacionAsync(fechaDesde, fechaHasta, esAccionista);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para generar el PDF." });
|
||||
|
||||
try
|
||||
{
|
||||
LocalReport report = new LocalReport();
|
||||
string rdlcPath = Path.Combine("Controllers", "Reportes", "RDLC", "ReporteListadoDistribucionCanMensual.rdlc");
|
||||
if (!System.IO.File.Exists(rdlcPath))
|
||||
{
|
||||
_logger.LogError("Archivo RDLC no encontrado: {Path}", rdlcPath);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"Archivo de reporte no encontrado: {Path.GetFileName(rdlcPath)}");
|
||||
}
|
||||
using (var fs = new FileStream(rdlcPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
report.LoadReportDefinition(fs);
|
||||
}
|
||||
report.DataSources.Add(new ReportDataSource("DSListadoDistribucionCanMensual", data));
|
||||
|
||||
var parameters = new List<ReportParameter>
|
||||
{
|
||||
new ReportParameter("FechaDesde", fechaDesde.ToString("dd/MM/yyyy")),
|
||||
new ReportParameter("FechaHasta", fechaHasta.ToString("dd/MM/yyyy")),
|
||||
new ReportParameter("CanAcc", esAccionista ? "1" : "0")
|
||||
};
|
||||
report.SetParameters(parameters);
|
||||
|
||||
byte[] pdfBytes = report.Render("PDF");
|
||||
string tipoDesc = esAccionista ? "Accionistas" : "Canillitas";
|
||||
return File(pdfBytes, "application/pdf", $"ListadoDistMensualPub_{tipoDesc}_{fechaDesde:yyyyMMdd}_{fechaHasta:yyyyMMdd}.pdf");
|
||||
}
|
||||
catch (Exception ex) { _logger.LogError(ex, "Error PDF ListadoDistMensualPorPublicacion"); return StatusCode(500, "Error interno."); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic; // Para IEnumerable
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Contables; // Para SaldoGestionDto si lo usas aquí
|
||||
using GestionIntegral.Api.Models.Contables; // Para Saldo, SaldoAjusteHistorial
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
{
|
||||
@@ -15,5 +17,12 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
// Método para modificar saldo (lo teníamos como privado antes, ahora en el repo)
|
||||
Task<bool> ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null);
|
||||
Task<bool> CheckIfSaldosExistForEmpresaAsync(int id);
|
||||
|
||||
// Para obtener la lista de saldos para la página de gestión
|
||||
Task<IEnumerable<Saldo>> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter);
|
||||
// Para obtener un saldo específico (ya podría existir uno similar, o crearlo si es necesario)
|
||||
Task<Saldo?> GetSaldoAsync(string destino, int idDestino, int idEmpresa, IDbTransaction? transaction = null);
|
||||
// Para registrar el historial de ajuste
|
||||
Task CreateSaldoAjusteHistorialAsync(SaldoAjusteHistorial historialEntry, IDbTransaction transaction);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
using GestionIntegral.Api.Models.Contables;
|
||||
using System.Text;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
{
|
||||
@@ -57,61 +59,64 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
|
||||
public async Task<bool> DeleteSaldosByEmpresaAsync(int idEmpresa, IDbTransaction transaction)
|
||||
{
|
||||
var sql = "DELETE FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa";
|
||||
try
|
||||
{
|
||||
await transaction.Connection!.ExecuteAsync(sql, new { IdEmpresa = idEmpresa }, transaction: transaction);
|
||||
return true; // Asumir éxito si no hay excepción
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al eliminar saldos para Empresa ID {IdEmpresa}.", idEmpresa);
|
||||
throw;
|
||||
}
|
||||
var sql = "DELETE FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa";
|
||||
try
|
||||
{
|
||||
await transaction.Connection!.ExecuteAsync(sql, new { IdEmpresa = idEmpresa }, transaction: transaction);
|
||||
return true; // Asumir éxito si no hay excepción
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al eliminar saldos para Empresa ID {IdEmpresa}.", idEmpresa);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null)
|
||||
{
|
||||
var sql = @"UPDATE dbo.cue_Saldos
|
||||
SET Monto = Monto + @MontoAAgregar
|
||||
WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;";
|
||||
{
|
||||
var sql = @"UPDATE dbo.cue_Saldos
|
||||
SET Monto = Monto + @MontoAAgregar,
|
||||
FechaUltimaModificacion = @FechaActualizacion -- << AÑADIR
|
||||
WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;";
|
||||
|
||||
// Usar una variable para la conexión para poder aplicar el '!' si es necesario
|
||||
IDbConnection connection = transaction?.Connection ?? _connectionFactory.CreateConnection();
|
||||
bool ownConnection = transaction == null; // Saber si necesitamos cerrar la conexión nosotros
|
||||
IDbConnection connection = transaction?.Connection ?? _connectionFactory.CreateConnection();
|
||||
bool ownConnection = transaction == null;
|
||||
|
||||
try
|
||||
{
|
||||
if (ownConnection) await (connection as System.Data.Common.DbConnection)!.OpenAsync(); // Abrir solo si no hay transacción externa
|
||||
try
|
||||
{
|
||||
if (ownConnection && connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
|
||||
var parameters = new {
|
||||
MontoAAgregar = montoAAgregar,
|
||||
Destino = destino,
|
||||
IdDestino = idDestino,
|
||||
IdEmpresa = idEmpresa
|
||||
};
|
||||
// Aplicar '!' aquí también si viene de la transacción
|
||||
int rowsAffected = await connection.ExecuteAsync(sql, parameters, transaction: transaction);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al modificar saldo para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa}.", destino, idDestino, idEmpresa);
|
||||
if (transaction != null) throw; // Re-lanzar si estamos en una transacción externa
|
||||
return false; // Devolver false si fue una operación aislada que falló
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cerrar la conexión solo si la abrimos nosotros (no había transacción externa)
|
||||
if (ownConnection && connection.State == ConnectionState.Open)
|
||||
{
|
||||
await (connection as System.Data.Common.DbConnection)!.CloseAsync();
|
||||
}
|
||||
// Disponer de la conexión si la creamos nosotros
|
||||
if(ownConnection) (connection as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
public async Task<bool> CheckIfSaldosExistForEmpresaAsync(int idEmpresa)
|
||||
var parameters = new
|
||||
{
|
||||
MontoAAgregar = montoAAgregar,
|
||||
Destino = destino,
|
||||
IdDestino = idDestino,
|
||||
IdEmpresa = idEmpresa,
|
||||
FechaActualizacion = DateTime.Now // O DateTime.UtcNow si prefieres
|
||||
};
|
||||
int rowsAffected = await connection.ExecuteAsync(sql, parameters, transaction: transaction);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al modificar saldo para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa}.", destino, idDestino, idEmpresa);
|
||||
if (transaction != null) throw;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ownConnection && connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
if (ownConnection && connection is IDisposable d) d.Dispose(); // Mejorar dispose
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CheckIfSaldosExistForEmpresaAsync(int idEmpresa)
|
||||
{
|
||||
var sql = "SELECT COUNT(1) FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa";
|
||||
try
|
||||
@@ -130,5 +135,58 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
// O podrías devolver true para ser más conservador si la verificación es crítica.
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<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 WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(destinoFilter))
|
||||
{
|
||||
sqlBuilder.Append(" AND Destino = @Destino");
|
||||
parameters.Add("Destino", destinoFilter);
|
||||
}
|
||||
if (idDestinoFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Id_Destino = @IdDestino");
|
||||
parameters.Add("IdDestino", idDestinoFilter.Value);
|
||||
}
|
||||
if (idEmpresaFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Id_Empresa = @IdEmpresa");
|
||||
parameters.Add("IdEmpresa", idEmpresaFilter.Value);
|
||||
}
|
||||
sqlBuilder.Append(" ORDER BY Destino, Id_Empresa, Id_Destino;");
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters);
|
||||
}
|
||||
|
||||
public async Task<Saldo?> GetSaldoAsync(string destino, int idDestino, int idEmpresa, IDbTransaction? transaction = null)
|
||||
{
|
||||
const string sql = "SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion FROM dbo.cue_Saldos WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;";
|
||||
var conn = transaction?.Connection ?? _connectionFactory.CreateConnection();
|
||||
if (transaction == null && conn.State != ConnectionState.Open) { if (conn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else conn.Open(); }
|
||||
|
||||
try
|
||||
{
|
||||
return await conn.QuerySingleOrDefaultAsync<Saldo>(sql, new { Destino = destino, IdDestino = idDestino, IdEmpresa = idEmpresa }, transaction);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (transaction == null && conn.State == ConnectionState.Open) { if (conn is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else conn.Close(); }
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateSaldoAjusteHistorialAsync(SaldoAjusteHistorial historialEntry, IDbTransaction transaction)
|
||||
{
|
||||
const string sql = @"
|
||||
INSERT INTO dbo.cue_SaldoAjustesHistorial
|
||||
(Destino, Id_Destino, Id_Empresa, MontoAjuste, SaldoAnterior, SaldoNuevo, Justificacion, FechaAjuste, Id_UsuarioAjuste)
|
||||
VALUES
|
||||
(@Destino, @IdDestino, @IdEmpresa, @MontoAjuste, @SaldoAnterior, @SaldoNuevo, @Justificacion, @FechaAjuste, @IdUsuarioAjuste);";
|
||||
|
||||
await transaction.Connection!.ExecuteAsync(sql, historialEntry, transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,51 +21,56 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(Canilla Canilla, string NombreZona, string NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos)
|
||||
public async Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(
|
||||
string? nomApeFilter,
|
||||
int? legajoFilter,
|
||||
bool? esAccionista,
|
||||
bool? soloActivos) // <<-- Parámetro aquí
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe, c.Parada, c.Id_Zona AS IdZona,
|
||||
c.Accionista, c.Obs, c.Empresa, c.Baja, c.FechaBaja,
|
||||
z.Nombre AS NombreZona,
|
||||
ISNULL(e.Nombre, 'N/A (Accionista)') AS NombreEmpresa
|
||||
FROM dbo.dist_dtCanillas c
|
||||
INNER JOIN dbo.dist_dtZonas z ON c.Id_Zona = z.Id_Zona
|
||||
LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa
|
||||
WHERE 1=1");
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var sqlBuilder = new System.Text.StringBuilder(@"
|
||||
SELECT c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe, c.Parada, c.Id_Zona AS IdZona,
|
||||
c.Accionista, c.Obs, c.Empresa, c.Baja, c.FechaBaja,
|
||||
z.Nombre AS NombreZona,
|
||||
e.Nombre AS NombreEmpresa
|
||||
FROM dbo.dist_dtCanillas c
|
||||
LEFT JOIN dbo.dist_dtZonas z ON c.Id_Zona = z.Id_Zona
|
||||
LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa
|
||||
WHERE 1=1 "); // Cláusula base para añadir AND fácilmente
|
||||
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (soloActivos.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(soloActivos.Value ? " AND c.Baja = 0" : " AND c.Baja = 1");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(nomApeFilter))
|
||||
{
|
||||
sqlBuilder.Append(" AND c.NomApe LIKE @NomApeParam");
|
||||
parameters.Add("NomApeParam", $"%{nomApeFilter}%");
|
||||
sqlBuilder.Append(" AND c.NomApe LIKE @NomApeFilter ");
|
||||
parameters.Add("NomApeFilter", $"%{nomApeFilter}%");
|
||||
}
|
||||
if (legajoFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND c.Legajo = @LegajoParam");
|
||||
parameters.Add("LegajoParam", legajoFilter.Value);
|
||||
sqlBuilder.Append(" AND c.Legajo = @LegajoFilter ");
|
||||
parameters.Add("LegajoFilter", legajoFilter.Value);
|
||||
}
|
||||
if (soloActivos.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND c.Baja = @BajaStatus ");
|
||||
parameters.Add("BajaStatus", !soloActivos.Value); // Si soloActivos es true, Baja debe ser false
|
||||
}
|
||||
|
||||
if (esAccionista.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND c.Accionista = @EsAccionista ");
|
||||
parameters.Add("EsAccionista", esAccionista.Value); // true para accionistas, false para no accionistas (canillitas)
|
||||
}
|
||||
|
||||
sqlBuilder.Append(" ORDER BY c.NomApe;");
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<Canilla, string, string, (Canilla, string, string)>(
|
||||
sqlBuilder.ToString(),
|
||||
(canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa),
|
||||
parameters,
|
||||
splitOn: "NombreZona,NombreEmpresa"
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener todos los Canillas. Filtros: NomApe='{NomApeFilter}', Legajo='{LegajoFilter}', SoloActivos='{SoloActivos}'", nomApeFilter, legajoFilter, soloActivos);
|
||||
return Enumerable.Empty<(Canilla, string, string)>();
|
||||
}
|
||||
var result = await connection.QueryAsync<Canilla, string, string, (Canilla, string?, string?)>(
|
||||
sqlBuilder.ToString(),
|
||||
(can, zona, emp) => (can, zona, emp),
|
||||
parameters,
|
||||
splitOn: "NombreZona,NombreEmpresa"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id)
|
||||
@@ -83,12 +88,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
try
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var result = await connection.QueryAsync<Canilla, string, string, (Canilla?, string?, string?)>(
|
||||
sql,
|
||||
(canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa),
|
||||
new { IdParam = id },
|
||||
splitOn: "NombreZona,NombreEmpresa"
|
||||
);
|
||||
var result = await connection.QueryAsync<Canilla, string, string, (Canilla?, string?, string?)>(
|
||||
sql,
|
||||
(canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa),
|
||||
new { IdParam = id },
|
||||
splitOn: "NombreZona,NombreEmpresa"
|
||||
);
|
||||
return result.SingleOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -160,9 +165,19 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdCanillaParam = insertedCanilla.IdCanilla, LegajoParam = insertedCanilla.Legajo, NomApeParam = insertedCanilla.NomApe, ParadaParam = insertedCanilla.Parada, IdZonaParam = insertedCanilla.IdZona,
|
||||
AccionistaParam = insertedCanilla.Accionista, ObsParam = insertedCanilla.Obs, EmpresaParam = insertedCanilla.Empresa, BajaParam = insertedCanilla.Baja, FechaBajaParam = insertedCanilla.FechaBaja,
|
||||
Id_UsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Creado"
|
||||
IdCanillaParam = insertedCanilla.IdCanilla,
|
||||
LegajoParam = insertedCanilla.Legajo,
|
||||
NomApeParam = insertedCanilla.NomApe,
|
||||
ParadaParam = insertedCanilla.Parada,
|
||||
IdZonaParam = insertedCanilla.IdZona,
|
||||
AccionistaParam = insertedCanilla.Accionista,
|
||||
ObsParam = insertedCanilla.Obs,
|
||||
EmpresaParam = insertedCanilla.Empresa,
|
||||
BajaParam = insertedCanilla.Baja,
|
||||
FechaBajaParam = insertedCanilla.FechaBaja,
|
||||
Id_UsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Creado"
|
||||
}, transaction);
|
||||
return insertedCanilla;
|
||||
}
|
||||
@@ -187,13 +202,21 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
(Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdCanillaParam, @LegajoParam, @NomApeParam, @ParadaParam, @IdZonaParam, @AccionistaParam, @ObsParam, @EmpresaParam, @BajaParam, @FechaBajaParam, @Id_UsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdCanillaParam = canillaActual.IdCanilla,
|
||||
LegajoParam = canillaActual.Legajo, NomApeParam = canillaActual.NomApe, ParadaParam = canillaActual.Parada, IdZonaParam = canillaActual.IdZona,
|
||||
AccionistaParam = canillaActual.Accionista, ObsParam = canillaActual.Obs, EmpresaParam = canillaActual.Empresa,
|
||||
BajaParam = canillaActual.Baja, FechaBajaParam = canillaActual.FechaBaja,
|
||||
Id_UsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Actualizado"
|
||||
LegajoParam = canillaActual.Legajo,
|
||||
NomApeParam = canillaActual.NomApe,
|
||||
ParadaParam = canillaActual.Parada,
|
||||
IdZonaParam = canillaActual.IdZona,
|
||||
AccionistaParam = canillaActual.Accionista,
|
||||
ObsParam = canillaActual.Obs,
|
||||
EmpresaParam = canillaActual.Empresa,
|
||||
BajaParam = canillaActual.Baja,
|
||||
FechaBajaParam = canillaActual.FechaBaja,
|
||||
Id_UsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Actualizado"
|
||||
}, transaction);
|
||||
|
||||
var rowsAffected = await connection.ExecuteAsync(sqlUpdate, canillaAActualizar, transaction);
|
||||
@@ -218,10 +241,19 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdCanillaParam = canillaActual.IdCanilla, LegajoParam = canillaActual.Legajo, NomApeParam = canillaActual.NomApe, ParadaParam = canillaActual.Parada, IdZonaParam = canillaActual.IdZona,
|
||||
AccionistaParam = canillaActual.Accionista, ObsParam = canillaActual.Obs, EmpresaParam = canillaActual.Empresa,
|
||||
BajaNuevaParam = darDeBaja, FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null),
|
||||
Id_UsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModHistParam = (darDeBaja ? "Baja" : "Alta")
|
||||
IdCanillaParam = canillaActual.IdCanilla,
|
||||
LegajoParam = canillaActual.Legajo,
|
||||
NomApeParam = canillaActual.NomApe,
|
||||
ParadaParam = canillaActual.Parada,
|
||||
IdZonaParam = canillaActual.IdZona,
|
||||
AccionistaParam = canillaActual.Accionista,
|
||||
ObsParam = canillaActual.Obs,
|
||||
EmpresaParam = canillaActual.Empresa,
|
||||
BajaNuevaParam = darDeBaja,
|
||||
FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null),
|
||||
Id_UsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModHistParam = (darDeBaja ? "Baja" : "Alta")
|
||||
}, transaction);
|
||||
|
||||
var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { BajaParam = darDeBaja, FechaBajaParam = (darDeBaja ? fechaBaja : null), IdCanillaParam = id }, transaction);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System; // Añadido para Exception
|
||||
@@ -62,6 +63,30 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync()
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
Id_Distribuidor AS IdDistribuidor, Nombre
|
||||
FROM dbo.dist_dtDistribuidores
|
||||
WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
sqlBuilder.Append(" ORDER BY Nombre;");
|
||||
try
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<DistribuidorDropdownDto>(
|
||||
sqlBuilder.ToString(),
|
||||
parameters
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener todos los Distribuidores.");
|
||||
return Enumerable.Empty<DistribuidorDropdownDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id)
|
||||
{
|
||||
const string sql = @"
|
||||
@@ -90,6 +115,25 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id)
|
||||
{
|
||||
const string sql = @"
|
||||
SELECT
|
||||
Id_Distribuidor AS IdDistribuidor, Nombre
|
||||
FROM dbo.dist_dtDistribuidores
|
||||
WHERE Id_Distribuidor = @IdParam";
|
||||
try
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<DistribuidorLookupDto>(sql, new { IdParam = id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener Distribuidor por ID: {IdDistribuidor}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Distribuidor?> GetByIdSimpleAsync(int id)
|
||||
{
|
||||
const string sql = @"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Data.Repositories;
|
||||
using GestionIntegral.Api.Dtos.Empresas;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
@@ -52,6 +53,25 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EmpresaDropdownDto>> GetAllDropdownAsync()
|
||||
{
|
||||
var sqlBuilder = new StringBuilder("SELECT Id_Empresa AS IdEmpresa, Nombre FROM dbo.dist_dtEmpresas WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
sqlBuilder.Append(" ORDER BY Nombre;");
|
||||
try
|
||||
{
|
||||
using (var connection = _connectionFactory.CreateConnection())
|
||||
{
|
||||
return await connection.QueryAsync<EmpresaDropdownDto>(sqlBuilder.ToString(), parameters);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener todas las Empresas.");
|
||||
return Enumerable.Empty<EmpresaDropdownDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Empresa?> GetByIdAsync(int id)
|
||||
{
|
||||
var sql = "SELECT Id_Empresa AS IdEmpresa, Nombre, Detalle FROM dbo.dist_dtEmpresas WHERE Id_Empresa = @Id";
|
||||
@@ -69,6 +89,23 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Empresa?> ObtenerLookupPorIdAsync(int id)
|
||||
{
|
||||
var sql = "SELECT Id_Empresa AS IdEmpresa, Nombre FROM dbo.dist_dtEmpresas WHERE Id_Empresa = @Id";
|
||||
try
|
||||
{
|
||||
using (var connection = _connectionFactory.CreateConnection())
|
||||
{
|
||||
return await connection.QuerySingleOrDefaultAsync<Empresa>(sql, new { Id = id });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener Empresa por ID: {IdEmpresa}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByNameAsync(string nombre, int? excludeId = null)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtEmpresas WHERE Nombre = @Nombre");
|
||||
@@ -144,7 +181,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
|
||||
// Insertar en historial
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new {
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdEmpresa = insertedEmpresa.IdEmpresa,
|
||||
insertedEmpresa.Nombre,
|
||||
insertedEmpresa.Detalle,
|
||||
@@ -172,7 +210,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
VALUES (@IdEmpresa, @NombreActual, @DetalleActual, @IdUsuario, @FechaMod, @TipoMod);";
|
||||
|
||||
// Insertar en historial (estado anterior)
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new {
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdEmpresa = empresaActual.IdEmpresa,
|
||||
NombreActual = empresaActual.Nombre,
|
||||
DetalleActual = empresaActual.Detalle,
|
||||
@@ -182,7 +221,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}, transaction: transaction);
|
||||
|
||||
// Actualizar principal
|
||||
var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new {
|
||||
var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new
|
||||
{
|
||||
empresaAActualizar.Nombre,
|
||||
empresaAActualizar.Detalle,
|
||||
empresaAActualizar.IdEmpresa
|
||||
@@ -202,7 +242,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
VALUES (@IdEmpresa, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);";
|
||||
|
||||
// Insertar en historial (estado antes de borrar)
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new {
|
||||
await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdEmpresa = empresaActual.IdEmpresa,
|
||||
empresaActual.Nombre,
|
||||
empresaActual.Detalle,
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public interface ICanillaRepository
|
||||
{
|
||||
Task<IEnumerable<(Canilla Canilla, string NombreZona, string NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos);
|
||||
Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista);
|
||||
Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id);
|
||||
Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla
|
||||
Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction);
|
||||
|
||||
@@ -2,6 +2,7 @@ using GestionIntegral.Api.Models.Distribucion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
@@ -16,5 +17,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
Task<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();
|
||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Data; // Para IDbTransaction
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Empresas; // Para IDbTransaction
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
@@ -14,5 +15,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Necesita transacción
|
||||
Task<bool> ExistsByNameAsync(string nombre, int? excludeId = null);
|
||||
Task<bool> IsInUseAsync(int id);
|
||||
Task<IEnumerable<EmpresaDropdownDto>> GetAllDropdownAsync();
|
||||
Task<Empresa?> ObtenerLookupPorIdAsync(int id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using GestionIntegral.Api.Dtos.Reportes;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data; // Para IDbTransaction
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public interface INovedadCanillaRepository
|
||||
{
|
||||
// Para obtener novedades y el nombre del canillita
|
||||
Task<IEnumerable<(NovedadCanilla Novedad, string NombreCanilla)>> GetByCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta);
|
||||
Task<NovedadCanilla?> GetByIdAsync(int idNovedad);
|
||||
Task<NovedadCanilla?> CreateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null);
|
||||
Task<bool> UpdateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null);
|
||||
Task<bool> DeleteAsync(int idNovedad, int idUsuario, IDbTransaction? transaction = null);
|
||||
// Podrías añadir un método para verificar si existe una novedad para un canillita en una fecha específica si es necesario
|
||||
Task<bool> ExistsByCanillaAndFechaAsync(int idCanilla, DateTime fecha, int? excludeIdNovedad = null);
|
||||
Task<IEnumerable<NovedadesCanillasReporteDto>> GetReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<CanillaGananciaReporteDto>> GetReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using Microsoft.Extensions.Configuration; // Para IConfiguration
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient; // O el proveedor de tu BD
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GestionIntegral.Api.Dtos.Reportes;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public class NovedadCanillaRepository : INovedadCanillaRepository
|
||||
{
|
||||
private readonly DbConnectionFactory _connectionFactory; // Inyecta tu DbConnectionFactory
|
||||
private readonly ILogger<NovedadCanillaRepository> _logger;
|
||||
|
||||
public NovedadCanillaRepository(DbConnectionFactory connectionFactory, ILogger<NovedadCanillaRepository> logger)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task LogHistorialAsync(NovedadCanilla novedadOriginal, int idUsuario, string tipoMod, IDbConnection connection, IDbTransaction? transaction)
|
||||
{
|
||||
var historial = new NovedadCanillaHistorial
|
||||
{
|
||||
IdNovedad = novedadOriginal.IdNovedad,
|
||||
IdCanilla = novedadOriginal.IdCanilla,
|
||||
Fecha = novedadOriginal.Fecha,
|
||||
Detalle = novedadOriginal.Detalle,
|
||||
IdUsuario = idUsuario,
|
||||
FechaMod = DateTime.Now,
|
||||
TipoMod = tipoMod
|
||||
};
|
||||
var sqlHistorial = @"
|
||||
INSERT INTO dbo.dist_dtNovedadesCanillas_H
|
||||
(Id_Novedad, Id_Canilla, Fecha, Detalle, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES
|
||||
(@IdNovedad, @IdCanilla, @Fecha, @Detalle, @IdUsuario, @FechaMod, @TipoMod);";
|
||||
await connection.ExecuteAsync(sqlHistorial, historial, transaction);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(NovedadCanilla Novedad, string NombreCanilla)>> GetByCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var sqlBuilder = new System.Text.StringBuilder(@"
|
||||
SELECT
|
||||
n.Id_Novedad AS IdNovedad,
|
||||
n.Id_Canilla AS IdCanilla,
|
||||
n.Fecha,
|
||||
n.Detalle,
|
||||
c.NomApe AS NombreCanilla
|
||||
FROM dbo.dist_dtNovedadesCanillas n
|
||||
JOIN dbo.dist_dtCanillas c ON n.Id_Canilla = c.Id_Canilla
|
||||
WHERE n.Id_Canilla = @IdCanilla");
|
||||
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("IdCanilla", idCanilla);
|
||||
|
||||
if (fechaDesde.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND n.Fecha >= @FechaDesde");
|
||||
parameters.Add("FechaDesde", fechaDesde.Value.Date); // Solo fecha, sin hora
|
||||
}
|
||||
if (fechaHasta.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND n.Fecha <= @FechaHasta");
|
||||
// Para incluir todo el día de fechaHasta
|
||||
parameters.Add("FechaHasta", fechaHasta.Value.Date.AddDays(1).AddTicks(-1));
|
||||
}
|
||||
sqlBuilder.Append(" ORDER BY n.Fecha DESC, n.Id_Novedad DESC;");
|
||||
|
||||
var result = await connection.QueryAsync<NovedadCanilla, string, (NovedadCanilla, string)>(
|
||||
sqlBuilder.ToString(),
|
||||
(novedad, nombreCanilla) => (novedad, nombreCanilla),
|
||||
parameters,
|
||||
splitOn: "NombreCanilla"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public async Task<NovedadCanilla?> GetByIdAsync(int idNovedad)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var sql = "SELECT Id_Novedad AS IdNovedad, Id_Canilla AS IdCanilla, Fecha, Detalle FROM dbo.dist_dtNovedadesCanillas WHERE Id_Novedad = @IdNovedad;";
|
||||
return await connection.QuerySingleOrDefaultAsync<NovedadCanilla>(sql, new { IdNovedad = idNovedad });
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByCanillaAndFechaAsync(int idCanilla, DateTime fecha, int? excludeIdNovedad = null)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var sqlBuilder = new System.Text.StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtNovedadesCanillas WHERE Id_Canilla = @IdCanilla AND Fecha = @Fecha");
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("IdCanilla", idCanilla);
|
||||
parameters.Add("Fecha", fecha.Date); // Comparar solo la fecha
|
||||
|
||||
if (excludeIdNovedad.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Id_Novedad != @ExcludeIdNovedad");
|
||||
parameters.Add("ExcludeIdNovedad", excludeIdNovedad.Value);
|
||||
}
|
||||
|
||||
var count = await connection.ExecuteScalarAsync<int>(sqlBuilder.ToString(), parameters);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task<NovedadCanilla?> CreateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null)
|
||||
{
|
||||
var sql = @"
|
||||
INSERT INTO dbo.dist_dtNovedadesCanillas (Id_Canilla, Fecha, Detalle)
|
||||
VALUES (@IdCanilla, @Fecha, @Detalle);
|
||||
SELECT CAST(SCOPE_IDENTITY() as int);";
|
||||
|
||||
IDbConnection conn = transaction?.Connection ?? _connectionFactory.CreateConnection();
|
||||
bool manageConnection = transaction == null; // Solo gestionar si no hay transacción externa
|
||||
|
||||
try
|
||||
{
|
||||
if (manageConnection && conn.State != ConnectionState.Open) await (conn as SqlConnection)!.OpenAsync();
|
||||
|
||||
var newId = await conn.QuerySingleAsync<int>(sql, novedad, transaction);
|
||||
novedad.IdNovedad = newId;
|
||||
await LogHistorialAsync(novedad, idUsuario, "Insertada", conn, transaction);
|
||||
return novedad;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al crear NovedadCanilla para Canilla ID: {IdCanilla}", novedad.IdCanilla);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (manageConnection && conn.State == ConnectionState.Open) conn.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(NovedadCanilla novedad, int idUsuario, IDbTransaction? transaction = null)
|
||||
{
|
||||
var novedadOriginal = await GetByIdAsync(novedad.IdNovedad); // Necesitamos el estado original para el log
|
||||
if (novedadOriginal == null) return false; // No se encontró
|
||||
|
||||
var sql = @"
|
||||
UPDATE dbo.dist_dtNovedadesCanillas SET
|
||||
Detalle = @Detalle
|
||||
-- No se permite cambiar IdCanilla ni Fecha de una novedad existente
|
||||
WHERE Id_Novedad = @IdNovedad;";
|
||||
|
||||
IDbConnection conn = transaction?.Connection ?? _connectionFactory.CreateConnection();
|
||||
bool manageConnection = transaction == null;
|
||||
|
||||
try
|
||||
{
|
||||
if (manageConnection && conn.State != ConnectionState.Open) await (conn as SqlConnection)!.OpenAsync();
|
||||
|
||||
await LogHistorialAsync(novedadOriginal, idUsuario, "Modificada", conn, transaction); // Log con datos ANTES de actualizar
|
||||
var affectedRows = await conn.ExecuteAsync(sql, novedad, transaction);
|
||||
return affectedRows > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al actualizar NovedadCanilla ID: {IdNovedad}", novedad.IdNovedad);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (manageConnection && conn.State == ConnectionState.Open) conn.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int idNovedad, int idUsuario, IDbTransaction? transaction = null)
|
||||
{
|
||||
var novedadOriginal = await GetByIdAsync(idNovedad);
|
||||
if (novedadOriginal == null) return false;
|
||||
|
||||
var sql = "DELETE FROM dbo.dist_dtNovedadesCanillas WHERE Id_Novedad = @IdNovedad;";
|
||||
|
||||
IDbConnection conn = transaction?.Connection ?? _connectionFactory.CreateConnection();
|
||||
bool manageConnection = transaction == null;
|
||||
|
||||
try
|
||||
{
|
||||
if (manageConnection && conn.State != ConnectionState.Open) await (conn as SqlConnection)!.OpenAsync();
|
||||
|
||||
await LogHistorialAsync(novedadOriginal, idUsuario, "Eliminada", conn, transaction);
|
||||
var affectedRows = await conn.ExecuteAsync(sql, new { IdNovedad = idNovedad }, transaction);
|
||||
return affectedRows > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al eliminar NovedadCanilla ID: {IdNovedad}", idNovedad);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (manageConnection && conn.State == ConnectionState.Open) conn.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<NovedadesCanillasReporteDto>> GetReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var parameters = new
|
||||
{
|
||||
idEmpresa,
|
||||
fechaDesde = fechaDesde.Date, // Enviar solo la fecha
|
||||
fechaHasta = fechaHasta.Date.AddDays(1).AddTicks(-1) // Para incluir todo el día hasta las 23:59:59.999...
|
||||
};
|
||||
// El nombre del SP en el archivo es SP_DistCanillasNovedades
|
||||
return await connection.QueryAsync<NovedadesCanillasReporteDto>(
|
||||
"dbo.SP_DistCanillasNovedades", // Asegúrate que el nombre del SP sea exacto
|
||||
parameters,
|
||||
commandType: CommandType.StoredProcedure
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CanillaGananciaReporteDto>> GetReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var parameters = new
|
||||
{
|
||||
idEmpresa,
|
||||
fechaDesde = fechaDesde.Date,
|
||||
fechaHasta = fechaHasta.Date // El SP SP_DistCanillasGanancias maneja el rango inclusivo directamente
|
||||
};
|
||||
return await connection.QueryAsync<CanillaGananciaReporteDto>(
|
||||
"dbo.SP_DistCanillasGanancias", // Nombre del SP
|
||||
parameters,
|
||||
commandType: CommandType.StoredProcedure
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,5 +43,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
Task<(IEnumerable<ListadoDistribucionDistSimpleDto> Simple, IEnumerable<ListadoDistribucionDistPromedioDiaDto> Promedios, string? Error)> ObtenerListadoDistribucionDistribuidoresAsync(int idDistribuidor, int idPublicacion, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<LiquidacionCanillaDetalleDto>> GetLiquidacionCanillaDetalleAsync(DateTime fecha, int idCanilla);
|
||||
Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla);
|
||||
Task<IEnumerable<ListadoDistCanMensualDiariosDto>> GetReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista);
|
||||
Task<IEnumerable<ListadoDistCanMensualPubDto>> GetReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista);
|
||||
}
|
||||
}
|
||||
@@ -481,39 +481,71 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LiquidacionCanillaDetalleDto>> GetLiquidacionCanillaDetalleAsync(DateTime fecha, int idCanilla)
|
||||
{
|
||||
const string spName = "dbo.SP_DistCanillasLiquidacion";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||
parameters.Add("@idCanilla", idCanilla, DbType.Int32);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<LiquidacionCanillaDetalleDto>(spName, parameters, commandType: CommandType.StoredProcedure);
|
||||
const string spName = "dbo.SP_DistCanillasLiquidacion";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||
parameters.Add("@idCanilla", idCanilla, DbType.Int32);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<LiquidacionCanillaDetalleDto>(spName, parameters, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Detalle. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla);
|
||||
return Enumerable.Empty<LiquidacionCanillaDetalleDto>();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Detalle. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla);
|
||||
return Enumerable.Empty<LiquidacionCanillaDetalleDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla)
|
||||
{
|
||||
const string spName = "dbo.SP_DistCanillasLiquidacionGanancias";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||
parameters.Add("@idCanilla", idCanilla, DbType.Int32);
|
||||
try
|
||||
public async Task<IEnumerable<LiquidacionCanillaGananciaDto>> GetLiquidacionCanillaGananciasAsync(DateTime fecha, int idCanilla)
|
||||
{
|
||||
const string spName = "dbo.SP_DistCanillasLiquidacionGanancias";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||
parameters.Add("@idCanilla", idCanilla, DbType.Int32);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<LiquidacionCanillaGananciaDto>(spName, parameters, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Ganancias. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla);
|
||||
return Enumerable.Empty<LiquidacionCanillaGananciaDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ListadoDistCanMensualDiariosDto>> GetReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista)
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<LiquidacionCanillaGananciaDto>(spName, parameters, commandType: CommandType.StoredProcedure);
|
||||
var parameters = new
|
||||
{
|
||||
fechaDesde = fechaDesde.Date,
|
||||
fechaHasta = fechaHasta.Date, // El SP parece manejar el rango incluyendo el último día
|
||||
accionista = esAccionista
|
||||
};
|
||||
return await connection.QueryAsync<ListadoDistCanMensualDiariosDto>(
|
||||
"dbo.SP_DistCanillasAccConImporteEntreFechasDiarios",
|
||||
parameters,
|
||||
commandType: CommandType.StoredProcedure
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
public async Task<IEnumerable<ListadoDistCanMensualPubDto>> GetReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista)
|
||||
{
|
||||
_logger.LogError(ex, "Error SP {SPName} para Liquidacion Canilla Ganancias. Fecha: {Fecha}, Canilla: {IdCanilla}", spName, fecha, idCanilla);
|
||||
return Enumerable.Empty<LiquidacionCanillaGananciaDto>();
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
var parameters = new
|
||||
{
|
||||
fechaDesde = fechaDesde.Date,
|
||||
fechaHasta = fechaHasta.Date,
|
||||
accionista = esAccionista
|
||||
};
|
||||
return await connection.QueryAsync<ListadoDistCanMensualPubDto>(
|
||||
"dbo.SP_DistCanillasAccConImporteEntreFechas",
|
||||
parameters,
|
||||
commandType: CommandType.StoredProcedure
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Backend/GestionIntegral.Api/Models/Contables/Saldo.cs
Normal file
9
Backend/GestionIntegral.Api/Models/Contables/Saldo.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
public class Saldo
|
||||
{
|
||||
public int IdSaldo { get; set; }
|
||||
public string Destino { get; set; } = string.Empty;
|
||||
public int IdDestino { get; set; }
|
||||
public decimal Monto { get; set; }
|
||||
public int IdEmpresa { get; set; }
|
||||
public DateTime FechaUltimaModificacion { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Models.Contables
|
||||
{
|
||||
public class SaldoAjusteHistorial
|
||||
{
|
||||
public int IdSaldoAjusteHist { get; set; } // PK, Identity
|
||||
public string Destino { get; set; } = string.Empty;
|
||||
public int IdDestino { get; set; }
|
||||
public int IdEmpresa { get; set; }
|
||||
public decimal MontoAjuste { get; set; } // El monto que se sumó/restó
|
||||
public decimal SaldoAnterior { get; set; }
|
||||
public decimal SaldoNuevo { get; set; }
|
||||
public string Justificacion { get; set; } = string.Empty;
|
||||
public DateTime FechaAjuste { get; set; }
|
||||
public int IdUsuarioAjuste { get; set; }
|
||||
// Podrías añadir NombreUsuarioAjuste si quieres desnormalizar o hacer un JOIN al consultar
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace GestionIntegral.Api.Models.Distribucion
|
||||
{
|
||||
public class NovedadCanilla
|
||||
{
|
||||
public int IdNovedad { get; set; }
|
||||
public int IdCanilla { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
public string? Detalle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace GestionIntegral.Api.Models.Distribucion
|
||||
{
|
||||
public class NovedadCanillaHistorial
|
||||
{
|
||||
// No tiene un ID propio, es una tabla de historial puro
|
||||
public int IdNovedad { get; set; } // FK a la novedad original
|
||||
public int IdCanilla { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
public string? Detalle { get; set; }
|
||||
public int IdUsuario { get; set; } // Quién hizo el cambio
|
||||
public DateTime FechaMod { get; set; } // Cuándo se hizo el cambio
|
||||
public required string TipoMod { get; set; } // "Insertada", "Modificada", "Eliminada"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
{
|
||||
public class AjusteSaldoRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "El tipo de destino es obligatorio ('Distribuidores' o 'Canillas').")]
|
||||
[RegularExpression("^(Distribuidores|Canillas)$", ErrorMessage = "Destino debe ser 'Distribuidores' o 'Canillas'.")]
|
||||
public string Destino { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "El ID del destinatario es obligatorio.")]
|
||||
[Range(1, int.MaxValue, ErrorMessage = "ID de Destinatario inválido.")]
|
||||
public int IdDestino { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El ID de la empresa es obligatorio.")]
|
||||
[Range(1, int.MaxValue, ErrorMessage = "ID de Empresa inválido.")]
|
||||
public int IdEmpresa { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El monto del ajuste es obligatorio.")]
|
||||
// Permitir montos negativos para disminuir deuda o positivos para aumentarla
|
||||
// No se usa Range aquí para permitir ambos signos. La validación de que no sea cero se puede hacer en el servicio.
|
||||
public decimal MontoAjuste { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La justificación del ajuste es obligatoria.")]
|
||||
[StringLength(250, MinimumLength = 5, ErrorMessage = "La justificación debe tener entre 5 y 250 caracteres.")]
|
||||
public string Justificacion { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Contables
|
||||
{
|
||||
public class SaldoGestionDto
|
||||
{
|
||||
public int IdSaldo { get; set; }
|
||||
public string Destino { get; set; } = string.Empty;
|
||||
public int IdDestino { get; set; }
|
||||
public string NombreDestinatario { get; set; } = string.Empty;
|
||||
public int IdEmpresa { get; set; }
|
||||
public string NombreEmpresa { get; set; } = string.Empty;
|
||||
public decimal Monto { get; set; }
|
||||
public DateTime FechaUltimaModificacion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class CreateNovedadCanillaDto
|
||||
{
|
||||
// IdCanilla se tomará de la ruta o de un campo oculto si el POST es a un endpoint genérico.
|
||||
// Por ahora, lo incluimos si el endpoint es /api/novedadescanilla
|
||||
[Required]
|
||||
public int IdCanilla { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime Fecha { get; set; }
|
||||
|
||||
[MaxLength(250)]
|
||||
public string? Detalle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class DistribuidorDropdownDto
|
||||
{
|
||||
public int IdDistribuidor { get; set; }
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class DistribuidorLookupDto
|
||||
{
|
||||
public int IdDistribuidor { get; set; }
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GestionIntegral.Api.Dtos.Empresas
|
||||
{
|
||||
public class EmpresaDropdownDto
|
||||
{
|
||||
public int IdEmpresa { get; set; }
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GestionIntegral.Api.Dtos.Empresas
|
||||
{
|
||||
public class EmpresaLookupDto
|
||||
{
|
||||
public int IdEmpresa { get; set; }
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class NovedadCanillaDto
|
||||
{
|
||||
public int IdNovedad { get; set; }
|
||||
public int IdCanilla { get; set; }
|
||||
public string NombreCanilla { get; set; } = string.Empty; // Para mostrar en UI
|
||||
public DateTime Fecha { get; set; }
|
||||
public string? Detalle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class UpdateNovedadCanillaDto
|
||||
{
|
||||
// No se permite cambiar IdCanilla ni Fecha
|
||||
[MaxLength(250)]
|
||||
public string? Detalle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace GestionIntegral.Api.Dtos.Reportes
|
||||
{
|
||||
public class CanillaGananciaReporteDto // Nuevo nombre para el DTO
|
||||
{
|
||||
public string Canilla { get; set; } = string.Empty; // NomApe del canillita
|
||||
public int? Legajo { get; set; }
|
||||
public int? Francos { get; set; }
|
||||
public int? Faltas { get; set; }
|
||||
public decimal? TotalRendir { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace GestionIntegral.Api.Dtos.Reportes
|
||||
{
|
||||
public class ListadoDistCanMensualDiariosDto
|
||||
{
|
||||
public string Canilla { get; set; } = string.Empty;
|
||||
public int? ElDia { get; set; } // Cantidad
|
||||
public int? ElPlata { get; set; } // Cantidad
|
||||
public int? Vendidos { get; set; } // Suma de ElDia y ElPlata (cantidades)
|
||||
public decimal? ImporteElDia { get; set; }
|
||||
public decimal? ImporteElPlata { get; set; }
|
||||
public decimal? ImporteTotal { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace GestionIntegral.Api.Dtos.Reportes
|
||||
{
|
||||
public class ListadoDistCanMensualPubDto
|
||||
{
|
||||
public string Publicacion { get; set; } = string.Empty;
|
||||
public string Canilla { get; set; } = string.Empty; // NomApe
|
||||
public int? TotalCantSalida { get; set; }
|
||||
public int? TotalCantEntrada { get; set; }
|
||||
public decimal? TotalRendir { get; set; }
|
||||
// No es necesario 'Vendidos' ya que el SP no lo devuelve directamente para esta variante,
|
||||
// pero se puede calcular en el frontend si es necesario (Llevados - Devueltos).
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Reportes
|
||||
{
|
||||
public class NovedadesCanillasReporteDto
|
||||
{
|
||||
public string NomApe { get; set; } = string.Empty; // Nombre del Canillita
|
||||
public DateTime Fecha { get; set; }
|
||||
public string? Detalle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,10 @@ builder.Services.AddScoped<IRitmoService, RitmoService>();
|
||||
builder.Services.AddScoped<ICancionRepository, CancionRepository>();
|
||||
builder.Services.AddScoped<ICancionService, CancionService>();
|
||||
builder.Services.AddScoped<IRadioListaService, RadioListaService>();
|
||||
builder.Services.AddScoped<INovedadCanillaRepository, NovedadCanillaRepository>();
|
||||
builder.Services.AddScoped<INovedadCanillaService, NovedadCanillaService>();
|
||||
// Servicio de Saldos
|
||||
builder.Services.AddScoped<ISaldoService, SaldoService>();
|
||||
// Repositorios de Reportes
|
||||
builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
|
||||
// Servicios de Reportes
|
||||
@@ -199,7 +203,7 @@ if (app.Environment.IsDevelopment())
|
||||
});
|
||||
}
|
||||
|
||||
// ¡¡¡NO USAR UseHttpsRedirection si tu API corre en HTTP!!!
|
||||
// ¡¡¡NO USAR UseHttpsRedirection si la API corre en HTTP!!!
|
||||
// Comenta o elimina la siguiente línea si SÓLO usas http://localhost:5183
|
||||
// app.UseHttpsRedirection();
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
public interface ISaldoService
|
||||
{
|
||||
Task<IEnumerable<SaldoGestionDto>> ObtenerSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter);
|
||||
Task<(bool Exito, string? Error, SaldoGestionDto? SaldoActualizado)> RealizarAjusteManualSaldoAsync(AjusteSaldoRequestDto ajusteDto, int idUsuarioAjuste);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
}
|
||||
else if (nota.Destino == "Canillas")
|
||||
{
|
||||
var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino); // Asumiendo que GetByIdAsync devuelve una tupla
|
||||
var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino);
|
||||
nombreDestinatario = canData.Canilla?.NomApe ?? "Canillita Desconocido";
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
|
||||
public async Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario)
|
||||
{
|
||||
// Validar Destinatario
|
||||
if (createDto.Destino == "Distribuidores")
|
||||
{
|
||||
if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDestino) == null)
|
||||
@@ -103,7 +102,7 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
}
|
||||
else if (createDto.Destino == "Canillas")
|
||||
{
|
||||
if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) // Asumiendo GetByIdSimpleAsync en ICanillaRepository
|
||||
if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null)
|
||||
return (null, "El canillita especificado no existe.");
|
||||
}
|
||||
else { return (null, "Tipo de destino inválido."); }
|
||||
@@ -124,19 +123,29 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
};
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
IDbTransaction? transaction = null;
|
||||
try
|
||||
{
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
var notaCreada = await _notaRepo.CreateAsync(nuevaNota, idUsuario, transaction);
|
||||
if (notaCreada == null) throw new DataException("Error al registrar la nota.");
|
||||
|
||||
// Afectar Saldo
|
||||
// Nota de Crédito: Disminuye la deuda del destinatario (monto positivo para el servicio de saldo)
|
||||
// Nota de Débito: Aumenta la deuda del destinatario (monto negativo para el servicio de saldo)
|
||||
decimal montoAjusteSaldo = createDto.Tipo == "Credito" ? createDto.Monto : -createDto.Monto;
|
||||
decimal montoParaSaldo;
|
||||
if (createDto.Tipo == "Credito")
|
||||
{
|
||||
montoParaSaldo = -createDto.Monto;
|
||||
}
|
||||
else
|
||||
{
|
||||
montoParaSaldo = createDto.Monto;
|
||||
}
|
||||
|
||||
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoAjusteSaldo, transaction);
|
||||
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoParaSaldo, transaction);
|
||||
if (!saldoActualizado) throw new DataException($"Error al actualizar el saldo para {notaCreada.Destino} ID {notaCreada.IdDestino}.");
|
||||
|
||||
transaction.Commit();
|
||||
@@ -145,32 +154,57 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearAsync NotaCreditoDebito."); }
|
||||
_logger.LogError(ex, "Error CrearAsync NotaCreditoDebito.");
|
||||
return (null, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto updateDto, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
IDbTransaction? transaction = null;
|
||||
try
|
||||
{
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
var notaExistente = await _notaRepo.GetByIdAsync(idNota);
|
||||
if (notaExistente == null) return (false, "Nota no encontrada.");
|
||||
if (notaExistente == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (false, "Nota no encontrada.");
|
||||
}
|
||||
|
||||
// Calcular diferencia de monto para ajustar saldo
|
||||
decimal montoOriginal = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;
|
||||
decimal montoNuevo = notaExistente.Tipo == "Credito" ? updateDto.Monto : -updateDto.Monto; // Tipo no cambia
|
||||
decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal;
|
||||
decimal impactoOriginalSaldo = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
|
||||
decimal impactoNuevoSaldo = notaExistente.Tipo == "Credito" ? -updateDto.Monto : updateDto.Monto;
|
||||
decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo;
|
||||
|
||||
notaExistente.Monto = updateDto.Monto;
|
||||
notaExistente.Observaciones = updateDto.Observaciones;
|
||||
var notaParaActualizarEnRepo = new NotaCreditoDebito
|
||||
{
|
||||
IdNota = notaExistente.IdNota,
|
||||
Destino = notaExistente.Destino,
|
||||
IdDestino = notaExistente.IdDestino,
|
||||
Referencia = notaExistente.Referencia,
|
||||
Tipo = notaExistente.Tipo,
|
||||
Fecha = notaExistente.Fecha,
|
||||
Monto = updateDto.Monto,
|
||||
Observaciones = updateDto.Observaciones,
|
||||
IdEmpresa = notaExistente.IdEmpresa
|
||||
};
|
||||
|
||||
var actualizado = await _notaRepo.UpdateAsync(notaExistente, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar la nota.");
|
||||
var actualizado = await _notaRepo.UpdateAsync(notaParaActualizarEnRepo, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar la nota en la base de datos.");
|
||||
|
||||
if (diferenciaAjusteSaldo != 0)
|
||||
{
|
||||
@@ -182,30 +216,45 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_logger.LogInformation("NotaC/D ID {Id} actualizada por Usuario ID {UserId}.", idNota, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); }
|
||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync NotaCreditoDebito (KeyNotFound)."); } return (false, "Nota no encontrada."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ActualizarAsync NotaC/D ID: {Id}", idNota);
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync NotaCreditoDebito."); }
|
||||
_logger.LogError(ex, "Error ActualizarAsync Nota C/D ID: {Id}", idNota);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EliminarAsync(int idNota, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
IDbTransaction? transaction = null;
|
||||
try
|
||||
{
|
||||
var notaExistente = await _notaRepo.GetByIdAsync(idNota);
|
||||
if (notaExistente == null) return (false, "Nota no encontrada.");
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
// Revertir el efecto en el saldo
|
||||
decimal montoReversion = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
|
||||
var notaExistente = await _notaRepo.GetByIdAsync(idNota);
|
||||
if (notaExistente == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (false, "Nota no encontrada.");
|
||||
}
|
||||
|
||||
decimal montoReversion = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;
|
||||
|
||||
var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction);
|
||||
if (!eliminado) throw new DataException("Error al eliminar la nota.");
|
||||
if (!eliminado) throw new DataException("Error al eliminar la nota de la base de datos.");
|
||||
|
||||
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, montoReversion, transaction);
|
||||
if (!saldoActualizado) throw new DataException("Error al revertir el saldo tras la eliminación de la nota.");
|
||||
@@ -214,13 +263,20 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_logger.LogInformation("NotaC/D ID {Id} eliminada y saldo revertido por Usuario ID {UserId}.", idNota, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); }
|
||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync NotaCreditoDebito (KeyNotFound)."); } return (false, "Nota no encontrada."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync NotaCreditoDebito."); }
|
||||
_logger.LogError(ex, "Error EliminarAsync NotaC/D ID: {Id}", idNota);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,6 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento))
|
||||
return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'.");
|
||||
|
||||
|
||||
var nuevoPago = new PagoDistribuidor
|
||||
{
|
||||
IdDistribuidor = createDto.IdDistribuidor,
|
||||
@@ -109,19 +108,29 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
};
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
IDbTransaction? transaction = null; // Declarar fuera para el finally
|
||||
try
|
||||
{
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
var pagoCreado = await _pagoRepo.CreateAsync(nuevoPago, idUsuario, transaction);
|
||||
if (pagoCreado == null) throw new DataException("Error al registrar el pago.");
|
||||
|
||||
// Afectar Saldo
|
||||
// Si TipoMovimiento es "Recibido", el monto DISMINUYE la deuda del distribuidor (monto positivo para el servicio de saldo).
|
||||
// Si TipoMovimiento es "Realizado" (empresa paga a distribuidor), el monto AUMENTA la deuda (monto negativo para el servicio de saldo).
|
||||
decimal montoAjusteSaldo = createDto.TipoMovimiento == "Recibido" ? createDto.Monto : -createDto.Monto;
|
||||
decimal montoParaSaldo;
|
||||
if (createDto.TipoMovimiento == "Recibido")
|
||||
{
|
||||
montoParaSaldo = -createDto.Monto;
|
||||
}
|
||||
else
|
||||
{
|
||||
montoParaSaldo = createDto.Monto;
|
||||
}
|
||||
|
||||
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoCreado.IdDistribuidor, pagoCreado.IdEmpresa, montoAjusteSaldo, transaction);
|
||||
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoCreado.IdDistribuidor, pagoCreado.IdEmpresa, montoParaSaldo, transaction);
|
||||
if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor.");
|
||||
|
||||
transaction.Commit();
|
||||
@@ -130,37 +139,63 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearAsync PagoDistribuidor."); }
|
||||
_logger.LogError(ex, "Error CrearAsync PagoDistribuidor.");
|
||||
return (null, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPago, UpdatePagoDistribuidorDto updateDto, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
IDbTransaction? transaction = null;
|
||||
try
|
||||
{
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
var pagoExistente = await _pagoRepo.GetByIdAsync(idPago);
|
||||
if (pagoExistente == null) return (false, "Pago no encontrado.");
|
||||
if (pagoExistente == null)
|
||||
{
|
||||
transaction.Rollback(); // Rollback si no se encuentra
|
||||
return (false, "Pago no encontrado.");
|
||||
}
|
||||
|
||||
if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (false, "Tipo de pago no válido.");
|
||||
}
|
||||
|
||||
// Calcular la diferencia de monto para ajustar el saldo
|
||||
decimal montoOriginal = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto;
|
||||
decimal montoNuevo = pagoExistente.TipoMovimiento == "Recibido" ? updateDto.Monto : -updateDto.Monto;
|
||||
decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal;
|
||||
decimal impactoOriginalSaldo = pagoExistente.TipoMovimiento == "Recibido" ? -pagoExistente.Monto : pagoExistente.Monto;
|
||||
decimal impactoNuevoSaldo = pagoExistente.TipoMovimiento == "Recibido" ? -updateDto.Monto : updateDto.Monto;
|
||||
decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo;
|
||||
|
||||
// Actualizar campos permitidos
|
||||
pagoExistente.Monto = updateDto.Monto;
|
||||
pagoExistente.IdTipoPago = updateDto.IdTipoPago;
|
||||
pagoExistente.Detalle = updateDto.Detalle;
|
||||
var pagoParaActualizarEnRepo = new PagoDistribuidor
|
||||
{
|
||||
IdPago = pagoExistente.IdPago,
|
||||
IdDistribuidor = pagoExistente.IdDistribuidor,
|
||||
Fecha = pagoExistente.Fecha,
|
||||
TipoMovimiento = pagoExistente.TipoMovimiento,
|
||||
Recibo = pagoExistente.Recibo,
|
||||
Monto = updateDto.Monto,
|
||||
IdTipoPago = updateDto.IdTipoPago,
|
||||
Detalle = updateDto.Detalle,
|
||||
IdEmpresa = pagoExistente.IdEmpresa
|
||||
};
|
||||
|
||||
var actualizado = await _pagoRepo.UpdateAsync(pagoExistente, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar el pago.");
|
||||
var actualizado = await _pagoRepo.UpdateAsync(pagoParaActualizarEnRepo, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar el pago en la base de datos.");
|
||||
|
||||
if (diferenciaAjusteSaldo != 0)
|
||||
{
|
||||
@@ -172,32 +207,45 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_logger.LogInformation("PagoDistribuidor ID {Id} actualizado por Usuario ID {UserId}.", idPago, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); }
|
||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync PagoDistribuidor."); }
|
||||
_logger.LogError(ex, "Error ActualizarAsync PagoDistribuidor ID: {Id}", idPago);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EliminarAsync(int idPago, int idUsuario)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
IDbTransaction? transaction = null;
|
||||
try
|
||||
{
|
||||
var pagoExistente = await _pagoRepo.GetByIdAsync(idPago);
|
||||
if (pagoExistente == null) return (false, "Pago no encontrado.");
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
}
|
||||
transaction = connection.BeginTransaction();
|
||||
|
||||
// Revertir el efecto en el saldo
|
||||
// Si fue "Recibido", el saldo disminuyó (montoAjusteSaldo fue +Monto). Al eliminar, revertimos sumando -Monto (o restando +Monto).
|
||||
// Si fue "Realizado", el saldo aumentó (montoAjusteSaldo fue -Monto). Al eliminar, revertimos sumando +Monto (o restando -Monto).
|
||||
decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? -pagoExistente.Monto : pagoExistente.Monto;
|
||||
var pagoExistente = await _pagoRepo.GetByIdAsync(idPago);
|
||||
if (pagoExistente == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (false, "Pago no encontrado.");
|
||||
}
|
||||
|
||||
decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto;
|
||||
|
||||
var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction);
|
||||
if (!eliminado) throw new DataException("Error al eliminar el pago.");
|
||||
if (!eliminado) throw new DataException("Error al eliminar el pago de la base de datos.");
|
||||
|
||||
bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, montoReversion, transaction);
|
||||
if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación del pago.");
|
||||
@@ -206,13 +254,20 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
_logger.LogInformation("PagoDistribuidor ID {Id} eliminado por Usuario ID {UserId}.", idPago, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); }
|
||||
catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor (KeyNotFound)."); } return (false, "Pago no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync PagoDistribuidor."); }
|
||||
_logger.LogError(ex, "Error EliminarAsync PagoDistribuidor ID: {Id}", idPago);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
Backend/GestionIntegral.Api/Services/Contables/SaldoService.cs
Normal file
164
Backend/GestionIntegral.Api/Services/Contables/SaldoService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using GestionIntegral.Api.Data;
|
||||
using GestionIntegral.Api.Data.Repositories.Contables;
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion; // Para IDistribuidorRepository, ICanillaRepository
|
||||
using GestionIntegral.Api.Dtos.Contables;
|
||||
using GestionIntegral.Api.Models.Contables;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
public class SaldoService : ISaldoService
|
||||
{
|
||||
private readonly ISaldoRepository _saldoRepo;
|
||||
private readonly IDistribuidorRepository _distribuidorRepo; // Para nombres
|
||||
private readonly ICanillaRepository _canillaRepo; // Para nombres
|
||||
private readonly IEmpresaRepository _empresaRepo; // Para nombres
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<SaldoService> _logger;
|
||||
|
||||
public SaldoService(
|
||||
ISaldoRepository saldoRepo,
|
||||
IDistribuidorRepository distribuidorRepo,
|
||||
ICanillaRepository canillaRepo,
|
||||
IEmpresaRepository empresaRepo,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<SaldoService> logger)
|
||||
{
|
||||
_saldoRepo = saldoRepo;
|
||||
_distribuidorRepo = distribuidorRepo;
|
||||
_canillaRepo = canillaRepo;
|
||||
_empresaRepo = empresaRepo;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<SaldoGestionDto> MapToGestionDto(Saldo saldo)
|
||||
{
|
||||
if (saldo == null) return null!;
|
||||
|
||||
string nombreDestinatario = "N/A";
|
||||
if (saldo.Destino == "Distribuidores")
|
||||
{
|
||||
var distData = await _distribuidorRepo.GetByIdAsync(saldo.IdDestino);
|
||||
nombreDestinatario = distData.Distribuidor?.Nombre ?? $"Dist. ID {saldo.IdDestino}";
|
||||
}
|
||||
else if (saldo.Destino == "Canillas")
|
||||
{
|
||||
var canData = await _canillaRepo.GetByIdAsync(saldo.IdDestino);
|
||||
nombreDestinatario = canData.Canilla?.NomApe ?? $"Can. ID {saldo.IdDestino}";
|
||||
}
|
||||
|
||||
var empresa = await _empresaRepo.GetByIdAsync(saldo.IdEmpresa);
|
||||
|
||||
return new SaldoGestionDto
|
||||
{
|
||||
IdSaldo = saldo.IdSaldo,
|
||||
Destino = saldo.Destino,
|
||||
IdDestino = saldo.IdDestino,
|
||||
NombreDestinatario = nombreDestinatario,
|
||||
IdEmpresa = saldo.IdEmpresa,
|
||||
NombreEmpresa = empresa?.Nombre ?? $"Emp. ID {saldo.IdEmpresa}",
|
||||
Monto = saldo.Monto,
|
||||
FechaUltimaModificacion = saldo.FechaUltimaModificacion
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SaldoGestionDto>> ObtenerSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter)
|
||||
{
|
||||
var saldos = await _saldoRepo.GetSaldosParaGestionAsync(destinoFilter, idDestinoFilter, idEmpresaFilter);
|
||||
var dtos = new List<SaldoGestionDto>();
|
||||
foreach (var saldo in saldos)
|
||||
{
|
||||
dtos.Add(await MapToGestionDto(saldo));
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error, SaldoGestionDto? SaldoActualizado)> RealizarAjusteManualSaldoAsync(AjusteSaldoRequestDto ajusteDto, int idUsuarioAjuste)
|
||||
{
|
||||
if (ajusteDto.MontoAjuste == 0)
|
||||
return (false, "El monto de ajuste no puede ser cero.", null);
|
||||
|
||||
// Validar existencia de Destino y Empresa
|
||||
if (ajusteDto.Destino == "Distribuidores")
|
||||
{
|
||||
if (await _distribuidorRepo.GetByIdSimpleAsync(ajusteDto.IdDestino) == null)
|
||||
return (false, "El distribuidor especificado no existe.", null);
|
||||
}
|
||||
else if (ajusteDto.Destino == "Canillas")
|
||||
{
|
||||
if (await _canillaRepo.GetByIdSimpleAsync(ajusteDto.IdDestino) == null)
|
||||
return (false, "El canillita especificado no existe.", null);
|
||||
} else {
|
||||
return (false, "Tipo de destino inválido.", null);
|
||||
}
|
||||
if (await _empresaRepo.GetByIdAsync(ajusteDto.IdEmpresa) == null)
|
||||
return (false, "La empresa especificada no existe.", null);
|
||||
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection.State != ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); }
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var saldoActual = await _saldoRepo.GetSaldoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, transaction);
|
||||
if (saldoActual == null)
|
||||
{
|
||||
// Podríamos crear el saldo aquí si no existe y se quiere permitir un ajuste sobre un saldo nuevo.
|
||||
// O devolver error. Por ahora, error.
|
||||
transaction.Rollback();
|
||||
return (false, "No se encontró un saldo existente para el destinatario y empresa especificados.", null);
|
||||
}
|
||||
|
||||
decimal saldoAnterior = saldoActual.Monto;
|
||||
|
||||
bool modificado = await _saldoRepo.ModificarSaldoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, ajusteDto.MontoAjuste, transaction);
|
||||
if (!modificado)
|
||||
{
|
||||
throw new DataException("No se pudo modificar el saldo principal.");
|
||||
}
|
||||
|
||||
// Obtener el saldo después de la modificación para el historial
|
||||
var saldoDespuesDeModificacion = await _saldoRepo.GetSaldoAsync(ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, transaction);
|
||||
if(saldoDespuesDeModificacion == null) throw new DataException("No se pudo obtener el saldo después de la modificación.");
|
||||
|
||||
|
||||
var historial = new SaldoAjusteHistorial
|
||||
{
|
||||
Destino = ajusteDto.Destino,
|
||||
IdDestino = ajusteDto.IdDestino,
|
||||
IdEmpresa = ajusteDto.IdEmpresa,
|
||||
MontoAjuste = ajusteDto.MontoAjuste,
|
||||
SaldoAnterior = saldoAnterior,
|
||||
SaldoNuevo = saldoDespuesDeModificacion.Monto, // saldoActual.Monto + ajusteDto.MontoAjuste,
|
||||
Justificacion = ajusteDto.Justificacion,
|
||||
FechaAjuste = DateTime.Now, // O UtcNow
|
||||
IdUsuarioAjuste = idUsuarioAjuste
|
||||
};
|
||||
await _saldoRepo.CreateSaldoAjusteHistorialAsync(historial, transaction);
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Ajuste manual de saldo realizado para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa} por Usuario ID {IdUsuarioAjuste}. Monto: {MontoAjuste}",
|
||||
ajusteDto.Destino, ajusteDto.IdDestino, ajusteDto.IdEmpresa, idUsuarioAjuste, ajusteDto.MontoAjuste);
|
||||
|
||||
var saldoDtoActualizado = await MapToGestionDto(saldoDespuesDeModificacion);
|
||||
return (true, null, saldoDtoActualizado);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx){ _logger.LogError(rbEx, "Error en Rollback de RealizarAjusteManualSaldoAsync."); }
|
||||
_logger.LogError(ex, "Error en RealizarAjusteManualSaldoAsync.");
|
||||
return (false, $"Error interno al realizar el ajuste: {ex.Message}", null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open) { if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos)
|
||||
public async Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos)
|
||||
{
|
||||
var canillasData = await _canillaRepository.GetAllAsync(nomApeFilter, legajoFilter, soloActivos);
|
||||
var data = await _canillaRepository.GetAllAsync(nomApeFilter, legajoFilter, soloActivos, esAccionista);
|
||||
// Filtrar nulos y asegurar al compilador que no hay nulos en la lista final
|
||||
return canillasData.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||
}
|
||||
|
||||
public async Task<CanillaDto?> ObtenerPorIdAsync(int id)
|
||||
@@ -81,11 +81,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
if (createDto.Empresa != 0) // Solo validar empresa si no es 0
|
||||
{
|
||||
var empresa = await _empresaRepository.GetByIdAsync(createDto.Empresa);
|
||||
if(empresa == null)
|
||||
{
|
||||
var empresa = await _empresaRepository.GetByIdAsync(createDto.Empresa);
|
||||
if (empresa == null)
|
||||
{
|
||||
return (null, "La empresa seleccionada no es válida.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORREGIDO: Usar directamente el valor booleano
|
||||
@@ -131,12 +131,20 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
nombreEmpresaParaDto = empresaData?.Nombre ?? "Empresa Desconocida";
|
||||
}
|
||||
|
||||
var dtoCreado = new CanillaDto {
|
||||
IdCanilla = canillaCreado.IdCanilla, Legajo = canillaCreado.Legajo, NomApe = canillaCreado.NomApe,
|
||||
Parada = canillaCreado.Parada, IdZona = canillaCreado.IdZona, NombreZona = zona.Nombre, // Usar nombre de zona ya obtenido
|
||||
Accionista = canillaCreado.Accionista, Obs = canillaCreado.Obs, Empresa = canillaCreado.Empresa,
|
||||
NombreEmpresa = nombreEmpresaParaDto,
|
||||
Baja = canillaCreado.Baja, FechaBaja = null
|
||||
var dtoCreado = new CanillaDto
|
||||
{
|
||||
IdCanilla = canillaCreado.IdCanilla,
|
||||
Legajo = canillaCreado.Legajo,
|
||||
NomApe = canillaCreado.NomApe,
|
||||
Parada = canillaCreado.Parada,
|
||||
IdZona = canillaCreado.IdZona,
|
||||
NombreZona = zona.Nombre, // Usar nombre de zona ya obtenido
|
||||
Accionista = canillaCreado.Accionista,
|
||||
Obs = canillaCreado.Obs,
|
||||
Empresa = canillaCreado.Empresa,
|
||||
NombreEmpresa = nombreEmpresaParaDto,
|
||||
Baja = canillaCreado.Baja,
|
||||
FechaBaja = null
|
||||
};
|
||||
|
||||
_logger.LogInformation("Canilla ID {IdCanilla} creado por Usuario ID {IdUsuario}.", canillaCreado.IdCanilla, idUsuario);
|
||||
@@ -144,7 +152,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch {}
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error CrearAsync Canilla: {NomApe}", createDto.NomApe);
|
||||
return (null, $"Error interno al crear el canillita: {ex.Message}");
|
||||
}
|
||||
@@ -165,11 +173,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
if (updateDto.Empresa != 0) // Solo validar empresa si no es 0
|
||||
{
|
||||
var empresa = await _empresaRepository.GetByIdAsync(updateDto.Empresa);
|
||||
if(empresa == null)
|
||||
{
|
||||
var empresa = await _empresaRepository.GetByIdAsync(updateDto.Empresa);
|
||||
if (empresa == null)
|
||||
{
|
||||
return (false, "La empresa seleccionada no es válida.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usar directamente el valor booleano para Accionista
|
||||
@@ -200,18 +208,19 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
try
|
||||
{
|
||||
var actualizado = await _canillaRepository.UpdateAsync(canillaExistente, idUsuario, transaction);
|
||||
if (!actualizado) throw new DataException("Error al actualizar el canillita.");
|
||||
if (!actualizado) throw new DataException("Error al actualizar el canillita.");
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Canilla ID {IdCanilla} actualizado por Usuario ID {IdUsuario}.", id, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) {
|
||||
try { transaction.Rollback(); } catch {}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
return (false, "Canillita no encontrado durante la actualización.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch {}
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ActualizarAsync Canilla ID: {IdCanilla}", id);
|
||||
return (false, $"Error interno al actualizar el canillita: {ex.Message}");
|
||||
}
|
||||
@@ -240,13 +249,14 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
_logger.LogInformation("Estado de baja cambiado a {EstadoBaja} para Canilla ID {IdCanilla} por Usuario ID {IdUsuario}.", darDeBaja, id, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) {
|
||||
try { transaction.Rollback(); } catch {}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
return (false, "Canillita no encontrado durante el cambio de estado de baja.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch {}
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ToggleBajaAsync Canilla ID: {IdCanilla}", id);
|
||||
return (false, $"Error interno al cambiar estado de baja: {ex.Message}");
|
||||
}
|
||||
|
||||
@@ -66,6 +66,20 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync()
|
||||
{
|
||||
var data = await _distribuidorRepository.GetAllDropdownAsync();
|
||||
// Asegurar que el resultado no sea nulo y no contiene elementos nulos
|
||||
if (data == null)
|
||||
{
|
||||
return new List<DistribuidorDropdownDto>
|
||||
{
|
||||
new DistribuidorDropdownDto { IdDistribuidor = 0, Nombre = "No hay distribuidores disponibles" }
|
||||
};
|
||||
}
|
||||
return data.Where(x => x != null)!;
|
||||
}
|
||||
|
||||
public async Task<DistribuidorDto?> ObtenerPorIdAsync(int id)
|
||||
{
|
||||
var data = await _distribuidorRepository.GetByIdAsync(id);
|
||||
@@ -73,6 +87,12 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
return MapToDto(data);
|
||||
}
|
||||
|
||||
public async Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id)
|
||||
{
|
||||
var data = await _distribuidorRepository.ObtenerLookupPorIdAsync(id);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario)
|
||||
{
|
||||
if (await _distribuidorRepository.ExistsByNroDocAsync(createDto.NroDoc))
|
||||
|
||||
@@ -43,9 +43,21 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EmpresaDropdownDto>> ObtenerParaDropdown()
|
||||
{
|
||||
// El repositorio ya devuelve solo las activas si es necesario
|
||||
var empresas = await _empresaRepository.GetAllDropdownAsync();
|
||||
// Mapeo Entidad -> DTO
|
||||
return empresas.Select(e => new EmpresaDropdownDto
|
||||
{
|
||||
IdEmpresa = e.IdEmpresa,
|
||||
Nombre = e.Nombre
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<EmpresaDto?> ObtenerPorIdAsync(int id)
|
||||
{
|
||||
// El repositorio ya devuelve solo las activas si es necesario
|
||||
// El repositorio ya devuelve solo las activas si es necesario
|
||||
var empresa = await _empresaRepository.GetByIdAsync(id);
|
||||
if (empresa == null) return null;
|
||||
// Mapeo Entidad -> DTO
|
||||
@@ -57,6 +69,19 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<EmpresaLookupDto?> ObtenerLookupPorIdAsync(int id)
|
||||
{
|
||||
// El repositorio ya devuelve solo las activas si es necesario
|
||||
var empresa = await _empresaRepository.ObtenerLookupPorIdAsync(id);
|
||||
if (empresa == null) return null;
|
||||
// Mapeo Entidad -> DTO
|
||||
return new EmpresaLookupDto
|
||||
{
|
||||
IdEmpresa = empresa.IdEmpresa,
|
||||
Nombre = empresa.Nombre
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<(EmpresaDto? Empresa, string? Error)> CrearAsync(CreateEmpresaDto createDto, int idUsuario)
|
||||
{
|
||||
// Validación de negocio: Nombre duplicado
|
||||
@@ -234,5 +259,5 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
// --- Fin Transacción ---
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public interface ICanillaService
|
||||
{
|
||||
Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos);
|
||||
Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos);
|
||||
Task<CanillaDto?> ObtenerPorIdAsync(int id);
|
||||
Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario);
|
||||
|
||||
@@ -11,5 +11,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
|
||||
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync();
|
||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
Task<(EmpresaDto? Empresa, string? Error)> CrearAsync(CreateEmpresaDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEmpresaDto updateDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
|
||||
Task<IEnumerable<EmpresaDropdownDto>> ObtenerParaDropdown();
|
||||
Task<EmpresaLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Dtos.Reportes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public interface INovedadCanillaService
|
||||
{
|
||||
Task<IEnumerable<NovedadCanillaDto>> ObtenerPorCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta);
|
||||
Task<NovedadCanillaDto?> ObtenerPorIdAsync(int idNovedad);
|
||||
Task<(NovedadCanillaDto? Novedad, string? Error)> CrearAsync(CreateNovedadCanillaDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int idNovedad, UpdateNovedadCanillaDto updateDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> EliminarAsync(int idNovedad, int idUsuario);
|
||||
Task<IEnumerable<NovedadesCanillasReporteDto>> ObtenerReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<CanillaGananciaReporteDto>> ObtenerReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// En Services/Distribucion (o donde corresponda)
|
||||
using GestionIntegral.Api.Data; // Para DbConnectionFactory
|
||||
using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Dtos.Reportes;
|
||||
using GestionIntegral.Api.Models.Distribucion; // Asegúrate que el modelo Canilla tenga NomApe
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data; // Para IDbTransaction
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public class NovedadCanillaService : INovedadCanillaService
|
||||
{
|
||||
private readonly INovedadCanillaRepository _novedadRepository;
|
||||
private readonly ICanillaRepository _canillaRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<NovedadCanillaService> _logger;
|
||||
|
||||
public NovedadCanillaService(
|
||||
INovedadCanillaRepository novedadRepository,
|
||||
ICanillaRepository canillaRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<NovedadCanillaService> logger)
|
||||
{
|
||||
_novedadRepository = novedadRepository;
|
||||
_canillaRepository = canillaRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private NovedadCanillaDto MapToDto((NovedadCanilla Novedad, string NombreCanilla) data)
|
||||
{
|
||||
return new NovedadCanillaDto
|
||||
{
|
||||
IdNovedad = data.Novedad.IdNovedad,
|
||||
IdCanilla = data.Novedad.IdCanilla,
|
||||
NombreCanilla = data.NombreCanilla, // Viene de la tupla en GetByCanillaAsync
|
||||
Fecha = data.Novedad.Fecha,
|
||||
Detalle = data.Novedad.Detalle
|
||||
};
|
||||
}
|
||||
private NovedadCanillaDto MapToDto(NovedadCanilla data, string nombreCanilla)
|
||||
{
|
||||
return new NovedadCanillaDto
|
||||
{
|
||||
IdNovedad = data.IdNovedad,
|
||||
IdCanilla = data.IdCanilla,
|
||||
NombreCanilla = nombreCanilla,
|
||||
Fecha = data.Fecha,
|
||||
Detalle = data.Detalle
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<NovedadCanillaDto>> ObtenerPorCanillaAsync(int idCanilla, DateTime? fechaDesde, DateTime? fechaHasta)
|
||||
{
|
||||
var data = await _novedadRepository.GetByCanillaAsync(idCanilla, fechaDesde, fechaHasta);
|
||||
return data.Select(MapToDto);
|
||||
}
|
||||
|
||||
public async Task<NovedadCanillaDto?> ObtenerPorIdAsync(int idNovedad)
|
||||
{
|
||||
var novedad = await _novedadRepository.GetByIdAsync(idNovedad);
|
||||
if (novedad == null) return null;
|
||||
|
||||
// Asumiendo que _canillaRepository.GetByIdAsync devuelve una tupla (Canilla? Canilla, ...)
|
||||
// O un DTO CanillaDto que tiene NomApe
|
||||
var canillaDataResult = await _canillaRepository.GetByIdAsync(novedad.IdCanilla);
|
||||
|
||||
// Ajusta esto según lo que realmente devuelva GetByIdAsync
|
||||
// Si devuelve CanillaDto:
|
||||
// string nombreCanilla = canillaDataResult?.NomApe ?? "Desconocido";
|
||||
// Si devuelve la tupla (Canilla? Canilla, string? NombreZona, string? NombreEmpresa):
|
||||
string nombreCanilla = canillaDataResult.Canilla?.NomApe ?? "Desconocido";
|
||||
|
||||
return MapToDto(novedad, nombreCanilla);
|
||||
}
|
||||
|
||||
public async Task<(NovedadCanillaDto? Novedad, string? Error)> CrearAsync(CreateNovedadCanillaDto createDto, int idUsuario)
|
||||
{
|
||||
// Asegúrate que GetByIdSimpleAsync devuelva un objeto Canilla o algo con NomApe
|
||||
var canilla = await _canillaRepository.GetByIdSimpleAsync(createDto.IdCanilla);
|
||||
if (canilla == null)
|
||||
{
|
||||
return (null, "El canillita especificado no existe.");
|
||||
}
|
||||
|
||||
var nuevaNovedad = new NovedadCanilla
|
||||
{
|
||||
IdCanilla = createDto.IdCanilla,
|
||||
Fecha = createDto.Fecha.Date,
|
||||
Detalle = createDto.Detalle
|
||||
};
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
// Abre la conexión explícitamente si no se usa una transacción externa
|
||||
if (connection is System.Data.Common.DbConnection dbConn && connection.State != ConnectionState.Open)
|
||||
{
|
||||
await dbConn.OpenAsync();
|
||||
}
|
||||
else if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var creada = await _novedadRepository.CreateAsync(nuevaNovedad, idUsuario, transaction);
|
||||
if (creada == null)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (null, "Error al guardar la novedad en la base de datos.");
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Novedad ID {IdNovedad} para Canilla ID {IdCanilla} creada por Usuario ID {UserId}.", creada.IdNovedad, creada.IdCanilla, idUsuario);
|
||||
// Asegúrate que 'canilla.NomApe' sea accesible. Si GetByIdSimpleAsync devuelve la entidad Canilla, esto está bien.
|
||||
return (MapToDto(creada, canilla.NomApe ?? "Canilla sin nombre"), null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error durante Rollback en CrearAsync NovedadCanilla."); }
|
||||
_logger.LogError(ex, "Error CrearAsync NovedadCanilla para Canilla ID: {IdCanilla}", createDto.IdCanilla);
|
||||
return (null, $"Error interno al crear la novedad: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open) connection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNovedad, UpdateNovedadCanillaDto updateDto, int idUsuario)
|
||||
{
|
||||
var existente = await _novedadRepository.GetByIdAsync(idNovedad);
|
||||
if (existente == null)
|
||||
{
|
||||
return (false, "Novedad no encontrada.");
|
||||
}
|
||||
|
||||
existente.Detalle = updateDto.Detalle;
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn && connection.State != ConnectionState.Open)
|
||||
{
|
||||
await dbConn.OpenAsync();
|
||||
}
|
||||
else if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var actualizado = await _novedadRepository.UpdateAsync(existente, idUsuario, transaction);
|
||||
if (!actualizado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (false, "Error al actualizar la novedad en la base de datos.");
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Novedad ID {IdNovedad} actualizada por Usuario ID {UserId}.", idNovedad, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error durante Rollback en ActualizarAsync NovedadCanilla."); }
|
||||
_logger.LogError(ex, "Error ActualizarAsync NovedadCanilla ID: {IdNovedad}", idNovedad);
|
||||
return (false, $"Error interno al actualizar la novedad: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open) connection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> EliminarAsync(int idNovedad, int idUsuario)
|
||||
{
|
||||
var existente = await _novedadRepository.GetByIdAsync(idNovedad);
|
||||
if (existente == null)
|
||||
{
|
||||
return (false, "Novedad no encontrada.");
|
||||
}
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn && connection.State != ConnectionState.Open)
|
||||
{
|
||||
await dbConn.OpenAsync();
|
||||
}
|
||||
else if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var eliminado = await _novedadRepository.DeleteAsync(idNovedad, idUsuario, transaction);
|
||||
if (!eliminado)
|
||||
{
|
||||
transaction.Rollback();
|
||||
return (false, "Error al eliminar la novedad de la base de datos.");
|
||||
}
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Novedad ID {IdNovedad} eliminada por Usuario ID {UserId}.", idNovedad, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error durante Rollback en EliminarAsync NovedadCanilla."); }
|
||||
_logger.LogError(ex, "Error EliminarAsync NovedadCanilla ID: {IdNovedad}", idNovedad);
|
||||
return (false, $"Error interno al eliminar la novedad: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection.State == ConnectionState.Open) connection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<NovedadesCanillasReporteDto>> ObtenerReporteNovedadesAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
// Podría añadir validaciones o lógica de negocio adicional si fuera necesario
|
||||
// antes de llamar al repositorio. Por ahora, es una llamada directa.
|
||||
try
|
||||
{
|
||||
return await _novedadRepository.GetReporteNovedadesAsync(idEmpresa, fechaDesde, fechaHasta);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener datos para el reporte de novedades de canillitas. Empresa: {IdEmpresa}, Desde: {FechaDesde}, Hasta: {FechaHasta}", idEmpresa, fechaDesde, fechaHasta);
|
||||
// Podría relanzar o devolver una lista vacía con un mensaje de error,
|
||||
// dependiendo de cómo quiera manejar los errores en la capa de servicio.
|
||||
// Por simplicidad, relanzamos para que el controlador lo maneje.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CanillaGananciaReporteDto>> ObtenerReporteGananciasAsync(int idEmpresa, DateTime fechaDesde, DateTime fechaHasta)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _novedadRepository.GetReporteGananciasAsync(idEmpresa, fechaDesde, fechaHasta);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener datos para el reporte de ganancias de canillitas. Empresa: {IdEmpresa}, Desde: {FechaDesde}, Hasta: {FechaHasta}", idEmpresa, fechaDesde, fechaHasta);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,5 +69,8 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
IEnumerable<LiquidacionCanillaGananciaDto> Ganancias,
|
||||
string? Error
|
||||
)> ObtenerDatosTicketLiquidacionAsync(DateTime fecha, int idCanilla);
|
||||
|
||||
Task<(IEnumerable<ListadoDistCanMensualDiariosDto> Data, string? Error)> ObtenerReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista);
|
||||
Task<(IEnumerable<ListadoDistCanMensualPubDto> Data, string? Error)> ObtenerReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista);
|
||||
}
|
||||
}
|
||||
@@ -490,5 +490,33 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<ListadoDistCanMensualDiariosDto> Data, string? Error)> ObtenerReporteMensualDiariosAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _reportesRepository.GetReporteMensualDiariosAsync(fechaDesde, fechaHasta, esAccionista);
|
||||
return (data, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener reporte mensual canillitas (diarios).");
|
||||
return (Enumerable.Empty<ListadoDistCanMensualDiariosDto>(), "Error al obtener datos del reporte (diarios).");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<ListadoDistCanMensualPubDto> Data, string? Error)> ObtenerReporteMensualPorPublicacionAsync(DateTime fechaDesde, DateTime fechaHasta, bool esAccionista)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _reportesRepository.GetReporteMensualPorPublicacionAsync(fechaDesde, fechaHasta, esAccionista);
|
||||
return (data, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al obtener reporte mensual canillitas (por publicación).");
|
||||
return (Enumerable.Empty<ListadoDistCanMensualPubDto>(), "Error al obtener datos del reporte (por publicación).");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+062cc05fd00a484e43f8b4ff022e53ac49670a78")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8fb94f8cefc3b498397ffcbb9b9a2e66c13b25b9")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","1I2C2FVhJyFRbvyuGXnropbTYN\u002BqpCoTcHfxWbfWF10="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","ko3Qcj1qg4o0KikPIBL6WHcUA8sCBGtBIyzr8DuluqQ="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
@@ -1 +1 @@
|
||||
{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","1I2C2FVhJyFRbvyuGXnropbTYN\u002BqpCoTcHfxWbfWF10="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["V/5slELlkFDzZ8iiVKV8Jt0Ia8AL5AZxPCWo9apx5lQ=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","\u002BzMwu5DIAA49kPmSydn2WMzj\u002Bdcf0MC3YakKoR6HwYg=","FUb20tYUiusFv5/KhAPdh2OB4ArUWiGApXbQJdx8tX0=","pTWqrhLBwEeWg1GsRlTKzfOAnT1JEklZ8F1/EYlc1Nk=","Hu0oNH4YYNcbnR5Ts4qd5yzC5j5JbY2kEDXces8V1vs=","TKMARE0bLM2dm9NOqxxWztnuqao5IvCh24TEHCtht6I=","84UEEMEbmmNwHVXD5Iw3dtKHTZC0Zqbk3rIRO\u002BxOq4o=","qfTzsJ\u002B5ilLyrc6EhNm61KkSH37yRi85MtgW1\u002BUD2Vo=","4ayt/JAApEOfr0yjg9szkYMPzSs6x2k3QEwmrK5RZVY=","d0weYwKWe3mH5R2BURuNLkAyytO/viA6zivv9AcIBtQ=","Ssyx6SvSGgWMOzhc9pQpk6f6\u002BmVbKQNKeDJbvVA2tjs=","FSqDybxILZmKXw160ANhj76usnM83geRrbPvJxr89OA=","k3qzLxTWHeeJhAuWKMdta6j24bmJ9BMRMjuFEEVCRu0=","x/sHyso3gy4zVCu3ljpnTYCqu8IGZNRok1JoXiabIP8=","fdI2RZZ9M9QOVHCYU5cE\u002BgVVuT7ssRbMzdXvX8rHofc=","8ePFhqKT0OT9nEg3b5T7COC81U\u002BQBcf\u002BindBGyMy6z0=","/ghcduGmSd1I25YtYli\u002BqxF0xuscxc4cTDkbEC6XYVA=","/a3YEu0oBUeA5Qr2VMdppqLuz4CQPWJt2JfBl2dtUwA=","jEO/q4IO3UFTWxlyFwRr7kbGWcTIiS\u002BClxx3kahX/Fk=","4iYOCKYvhsROdGkA1hINVBejb6r8IkwFj9SNMKub3DM=","CeDswsZIn5a7t\u002BKeHJA222yhFvDVVEW1ky98Xxnxebc=","50j34YXOc950QSqaQBMtgezD3tV5mWWR9c5qZcYQoz4=","W/aX9jIKpjNEVoGrU6RXFOY8SDJVT6XB4Rg4QCaeQkQ=","16IbB\u002B3zYHZvsWbCQK6hBFmKJ6Z28SecBn2jm8R3w8I=","COJtHNQqycTJqXkFv2hhpLUT\u002B/AD4IWyQlmxkUVQPNk=","cp6a5bdvkLnUn3x47KQODzPycnx57RmWO\u002B9q8MuoGQo=","oKZRNhIQRaZrETEa3L6JiwIp0\u002BmjzJo193EWBoCuVUg=","sjwbCAEQX51sEWhYVGBihWUNBxniUKZALVJIGK\u002BYgsk=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","ko3Qcj1qg4o0KikPIBL6WHcUA8sCBGtBIyzr8DuluqQ="],"CachedAssets":{},"CachedCopyCandidates":{}}
|
||||
162
Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx
Normal file
162
Frontend/src/components/Modals/Contables/AjusteSaldoModal.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert, InputAdornment
|
||||
} from '@mui/material';
|
||||
import type { SaldoGestionDto } from '../../../models/dtos/Contables/SaldoGestionDto';
|
||||
import type { AjusteSaldoRequestDto } from '../../../models/dtos/Contables/AjusteSaldoRequestDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 450 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 3,
|
||||
};
|
||||
|
||||
interface AjusteSaldoModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: AjusteSaldoRequestDto) => Promise<void>; // El padre maneja la recarga
|
||||
saldoParaAjustar: SaldoGestionDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const AjusteSaldoModal: React.FC<AjusteSaldoModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
saldoParaAjustar,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [montoAjuste, setMontoAjuste] = useState<string>('');
|
||||
const [justificacion, setJustificacion] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMontoAjuste('');
|
||||
setJustificacion('');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
const numMontoAjuste = parseFloat(montoAjuste);
|
||||
|
||||
if (!montoAjuste.trim()) {
|
||||
errors.montoAjuste = 'El monto de ajuste es obligatorio.';
|
||||
} else if (isNaN(numMontoAjuste)) {
|
||||
errors.montoAjuste = 'El monto debe ser un número.';
|
||||
} else if (numMontoAjuste === 0) {
|
||||
errors.montoAjuste = 'El monto de ajuste no puede ser cero.';
|
||||
}
|
||||
|
||||
if (!justificacion.trim()) {
|
||||
errors.justificacion = 'La justificación es obligatoria.';
|
||||
} else if (justificacion.trim().length < 5 || justificacion.trim().length > 250) {
|
||||
errors.justificacion = 'La justificación debe tener entre 5 y 250 caracteres.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: 'montoAjuste' | 'justificacion') => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate() || !saldoParaAjustar) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const dataToSubmit: AjusteSaldoRequestDto = {
|
||||
destino: saldoParaAjustar.destino as 'Distribuidores' | 'Canillas',
|
||||
idDestino: saldoParaAjustar.idDestino,
|
||||
idEmpresa: saldoParaAjustar.idEmpresa,
|
||||
montoAjuste: parseFloat(montoAjuste),
|
||||
justificacion,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
onClose(); // Cerrar en éxito (el padre recargará)
|
||||
} catch (error: any) {
|
||||
// El error de API es manejado por la página padre
|
||||
console.error("Error en submit de AjusteSaldoModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!saldoParaAjustar) return null;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Ajustar Saldo Manualmente
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Destinatario: <strong>{saldoParaAjustar.nombreDestinatario}</strong> ({saldoParaAjustar.destino})
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Empresa: <strong>{saldoParaAjustar.nombreEmpresa}</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Saldo Actual: <strong>{saldoParaAjustar.monto.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}</strong>
|
||||
</Typography>
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label="Monto de Ajuste (+/-)"
|
||||
type="number"
|
||||
fullWidth
|
||||
required
|
||||
value={montoAjuste}
|
||||
onChange={(e) => { setMontoAjuste(e.target.value); handleInputChange('montoAjuste'); }}
|
||||
margin="normal"
|
||||
error={!!localErrors.montoAjuste}
|
||||
helperText={localErrors.montoAjuste || 'Ingrese un valor positivo para aumentar deuda o negativo para disminuirla.'}
|
||||
disabled={loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }}
|
||||
inputProps={{ step: "0.01" }}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
label="Justificación del Ajuste"
|
||||
fullWidth
|
||||
required
|
||||
value={justificacion}
|
||||
onChange={(e) => { setJustificacion(e.target.value); handleInputChange('justificacion'); }}
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={3}
|
||||
error={!!localErrors.justificacion}
|
||||
helperText={localErrors.justificacion || ''}
|
||||
disabled={loading}
|
||||
inputProps={{ maxLength: 250 }}
|
||||
/>
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 1 }}>{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} /> : 'Aplicar Ajuste'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AjusteSaldoModal;
|
||||
@@ -8,14 +8,14 @@ import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||
import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||
import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto';
|
||||
// import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Ya no es necesario cargar todos los canillitas aquí
|
||||
import publicacionService from '../../../services/Distribucion/publicacionService';
|
||||
import canillaService from '../../../services/Distribucion/canillaService';
|
||||
// import canillaService from '../../../services/Distribucion/canillaService'; // Ya no es necesario
|
||||
import entradaSalidaCanillaService from '../../../services/Distribucion/entradaSalidaCanillaService';
|
||||
import type { CreateBulkEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto';
|
||||
import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaItemDto';
|
||||
import axios from 'axios';
|
||||
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
@@ -34,14 +34,21 @@ const modalStyle = {
|
||||
interface EntradaSalidaCanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
// El onSubmit de la página padre se usa solo para edición. La creación se maneja internamente.
|
||||
onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise<void>;
|
||||
initialData?: EntradaSalidaCanillaDto | null;
|
||||
initialData?: EntradaSalidaCanillaDto | null; // Para edición
|
||||
prefillData?: { // Para creación, prellenar desde la página padre
|
||||
fecha?: string; // YYYY-MM-DD
|
||||
idCanilla?: number | string;
|
||||
nombreCanilla?: string; // << AÑADIR NOMBRE PARA MOSTRAR
|
||||
idPublicacion?: number | string; // Para pre-seleccionar la primera publicación en la lista de items
|
||||
} | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
interface FormRowItem {
|
||||
id: string;
|
||||
id: string; // ID temporal para el frontend
|
||||
idPublicacion: number | string;
|
||||
cantSalida: string;
|
||||
cantEntrada: string;
|
||||
@@ -50,56 +57,62 @@ interface FormRowItem {
|
||||
|
||||
const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps> = ({
|
||||
open,
|
||||
onClose, // Este onClose es el que se pasa desde GestionarEntradasSalidasCanillaPage
|
||||
onSubmit, // Este onSubmit es el que se pasa para la lógica de EDICIÓN
|
||||
onClose,
|
||||
onSubmit: onSubmitEdit, // Renombrar para claridad, ya que solo se usa para editar
|
||||
initialData,
|
||||
prefillData,
|
||||
errorMessage: parentErrorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [idCanilla, setIdCanilla] = useState<number | string>('');
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>('');
|
||||
// Estados para los campos que SÍ son editables o parte del formulario de items
|
||||
const [editIdPublicacion, setEditIdPublicacion] = useState<number | string>(''); // Solo para modo edición
|
||||
const [editCantSalida, setEditCantSalida] = useState<string>('0');
|
||||
const [editCantEntrada, setEditCantEntrada] = useState<string>('0');
|
||||
const [editObservacion, setEditObservacion] = useState('');
|
||||
const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); // Iniciar con una fila
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false); // Loading para submit
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Loading para canillas/pubs
|
||||
const [loadingItems, setLoadingItems] = useState(false); // Loading para pre-carga de items
|
||||
const [items, setItems] = useState<FormRowItem[]>([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
|
||||
// Estados para datos de dropdowns
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); // Sigue siendo necesario para la lista de items
|
||||
|
||||
// Estados de carga y error
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false); // Solo para publicaciones
|
||||
const [loadingItems, setLoadingItems] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
const [modalSpecificApiError, setModalSpecificApiError] = useState<string | null>(null);
|
||||
|
||||
const isEditing = Boolean(initialData);
|
||||
const isEditing = Boolean(initialData && initialData.idParte);
|
||||
|
||||
// Efecto para cargar datos de dropdowns (Publicaciones, Canillitas) SOLO UNA VEZ o cuando open cambia a true
|
||||
// Datos que vienen prellenados y no son editables en el modal (Fecha y Canillita)
|
||||
const displayFecha = isEditing ? (initialData?.fecha ? initialData.fecha.split('T')[0] : '') : (prefillData?.fecha || '');
|
||||
const displayIdCanilla = isEditing ? initialData?.idCanilla : prefillData?.idCanilla;
|
||||
const displayNombreCanilla = isEditing ? initialData?.nomApeCanilla : prefillData?.nombreCanilla;
|
||||
|
||||
|
||||
// Cargar publicaciones para el dropdown de items
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
const fetchPublicacionesDropdown = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: null }));
|
||||
try {
|
||||
const [pubsData, canillitasData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
canillaService.getAllCanillas(undefined, undefined, true)
|
||||
]);
|
||||
// Usar getPublicacionesForDropdown si lo tienes, sino getAllPublicaciones
|
||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicaciones(pubsData);
|
||||
setCanillitas(canillitasData);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios (publicaciones/canillitas).' }));
|
||||
console.error("Error al cargar publicaciones para dropdown", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar publicaciones.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchDropdownData();
|
||||
fetchPublicacionesDropdown();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
|
||||
// Efecto para inicializar el formulario cuando se abre o cambia initialData
|
||||
// Inicializar formulario y/o pre-cargar items
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
clearErrorMessage();
|
||||
@@ -107,65 +120,90 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
setLocalErrors({});
|
||||
|
||||
if (isEditing && initialData) {
|
||||
setIdCanilla(initialData.idCanilla || '');
|
||||
setFecha(initialData.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]);
|
||||
setEditIdPublicacion(initialData.idPublicacion || '');
|
||||
setEditCantSalida(initialData.cantSalida?.toString() || '0');
|
||||
setEditCantEntrada(initialData.cantEntrada?.toString() || '0');
|
||||
setEditObservacion(initialData.observacion || '');
|
||||
setItems([]); // En modo edición, no pre-cargamos items de la lista
|
||||
} else {
|
||||
// Modo NUEVO: resetear campos principales y dejar que el efecto de 'fecha' cargue los items
|
||||
setIdCanilla('');
|
||||
setFecha(new Date().toISOString().split('T')[0]); // Fecha actual por defecto
|
||||
// Los items se cargarán por el siguiente useEffect basado en la fecha
|
||||
setItems([]); // No hay lista de items en modo edición de un solo movimiento
|
||||
} else { // Modo Creación
|
||||
// Limpiar campos de edición
|
||||
setEditIdPublicacion('');
|
||||
setEditCantSalida('0');
|
||||
setEditCantEntrada('0');
|
||||
setEditObservacion('');
|
||||
|
||||
// Lógica para pre-cargar items basada en displayFecha y prefillData.idPublicacion
|
||||
// (Esta lógica se mueve al siguiente useEffect que depende de displayFecha y publicaciones)
|
||||
// Por ahora, solo aseguramos que `items` se resetee si es necesario.
|
||||
const idPubPrefill = prefillData?.idPublicacion;
|
||||
if (idPubPrefill && publicaciones.length > 0) {
|
||||
// Si ya tenemos publicaciones, y un prefill de publicación, intentamos setearlo
|
||||
const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay();
|
||||
setLoadingItems(true);
|
||||
publicacionService.getPublicacionesPorDiaSemana(diaSemana)
|
||||
.then(pubsPorDefecto => {
|
||||
let itemsIniciales: FormRowItem[];
|
||||
if (pubsPorDefecto.find(p => p.idPublicacion === Number(idPubPrefill))) {
|
||||
// Si la publicación prellenada está en las de por defecto, la usamos
|
||||
itemsIniciales = [{ id: Date.now().toString(), idPublicacion: Number(idPubPrefill), cantSalida: '0', cantEntrada: '0', observacion: '' }];
|
||||
} else if (pubsPorDefecto.length > 0) {
|
||||
// Si no, pero hay otras por defecto, usamos la primera de ellas
|
||||
itemsIniciales = pubsPorDefecto.map(pub => ({
|
||||
id: `${Date.now().toString()}-${pub.idPublicacion}`,
|
||||
idPublicacion: pub.idPublicacion,
|
||||
cantSalida: '0', cantEntrada: '0', observacion: ''
|
||||
}));
|
||||
} else {
|
||||
// Si no hay ninguna por defecto, y la prellenada no aplica, usamos la prellenada sola o vacía
|
||||
itemsIniciales = [{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }];
|
||||
}
|
||||
setItems(itemsIniciales.length > 0 ? itemsIniciales : [{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
})
|
||||
.catch(() => setItems([{ id: Date.now().toString(), idPublicacion: idPubPrefill || '', cantSalida: '0', cantEntrada: '0', observacion: '' }])) // Fallback
|
||||
.finally(() => setLoadingItems(false));
|
||||
} else if (publicaciones.length === 0 && !loadingDropdowns) { // Si no hay prefill de pub o no hay pubs aún
|
||||
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [open, initialData, isEditing, clearErrorMessage]);
|
||||
}, [open, initialData, isEditing, prefillData, clearErrorMessage, publicaciones, loadingDropdowns, displayFecha]); // Añadir displayFecha
|
||||
|
||||
|
||||
// Efecto para pre-cargar/re-cargar items cuando cambia la FECHA (en modo NUEVO)
|
||||
// y cuando las publicaciones están disponibles.
|
||||
// Efecto para pre-cargar items por defecto cuando cambia la FECHA (displayFecha) en modo NUEVO
|
||||
useEffect(() => {
|
||||
if (open && !isEditing && publicaciones.length > 0 && fecha) { // Asegurarse que 'fecha' tiene un valor
|
||||
const diaSemana = new Date(fecha + 'T00:00:00Z').getUTCDay(); // Usar UTC para getDay consistente
|
||||
setLoadingItems(true); // Indicador de carga para los items
|
||||
setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
|
||||
if (open && !isEditing && publicaciones.length > 0 && displayFecha) {
|
||||
const diaSemana = new Date(displayFecha + 'T00:00:00Z').getUTCDay();
|
||||
setLoadingItems(true);
|
||||
publicacionService.getPublicacionesPorDiaSemana(diaSemana)
|
||||
.then(pubsPorDefecto => {
|
||||
if (pubsPorDefecto.length > 0) {
|
||||
const itemsPorDefecto = pubsPorDefecto.map(pub => ({
|
||||
id: `${Date.now().toString()}-${pub.idPublicacion}`,
|
||||
idPublicacion: pub.idPublicacion,
|
||||
cantSalida: '0',
|
||||
cantEntrada: '0',
|
||||
observacion: ''
|
||||
}));
|
||||
setItems(itemsPorDefecto);
|
||||
} else {
|
||||
// Si no hay configuraciones para el día, iniciar con una fila vacía
|
||||
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
}
|
||||
const itemsPorDefecto = pubsPorDefecto.map(pub => ({
|
||||
id: `${Date.now().toString()}-${pub.idPublicacion}`,
|
||||
idPublicacion: pub.idPublicacion,
|
||||
cantSalida: '0',
|
||||
cantEntrada: '0',
|
||||
observacion: ''
|
||||
}));
|
||||
setItems(itemsPorDefecto.length > 0 ? itemsPorDefecto : [{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error al cargar/recargar publicaciones por defecto para el día:", err);
|
||||
console.error("Error al cargar publicaciones por defecto para el día:", err);
|
||||
setLocalErrors(prev => ({ ...prev, general: 'Error al pre-cargar publicaciones del día.' }));
|
||||
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
})
|
||||
.finally(() => setLoadingItems(false));
|
||||
} else if (open && !isEditing && publicaciones.length === 0 && !loadingDropdowns) {
|
||||
// Si las publicaciones aún no se cargaron pero los dropdowns terminaron de cargar, iniciar con 1 item vacío.
|
||||
setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
}
|
||||
}, [open, isEditing, fecha, publicaciones, loadingDropdowns]); // Dependencias clave
|
||||
}, [open, isEditing, displayFecha, publicaciones]); // Dependencia de displayFecha y publicaciones
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
// ... (lógica de validación sin cambios, pero 'idCanilla' y 'fecha' ya no son estados del modal)
|
||||
const currentErrors: { [key: string]: string | null } = {};
|
||||
if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.';
|
||||
if (!fecha.trim()) currentErrors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
|
||||
// Validar displayIdCanilla y displayFecha si es modo creación
|
||||
if (!isEditing) {
|
||||
if (!displayIdCanilla) currentErrors.idCanilla = 'El canillita es obligatorio (provisto por la página).';
|
||||
if (!displayFecha || !displayFecha.trim()) currentErrors.fecha = 'La fecha es obligatoria (provista por la página).';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(displayFecha)) currentErrors.fecha = 'Formato de fecha inválido.';
|
||||
}
|
||||
// ... resto de la validación para items (modo creación) o campos edit (modo edición) ...
|
||||
if (isEditing) {
|
||||
const salidaNum = parseInt(editCantSalida, 10);
|
||||
const entradaNum = parseInt(editCantEntrada, 10);
|
||||
@@ -177,14 +215,15 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
} else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) {
|
||||
currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.';
|
||||
}
|
||||
} else {
|
||||
if (!editIdPublicacion) { // En edición, la publicación es fija, pero debe existir
|
||||
currentErrors.editIdPublicacion = 'Error: Publicación no especificada para edición.';
|
||||
}
|
||||
} else { // Modo Creación (Bulk)
|
||||
let hasValidItemWithQuantityOrPub = false;
|
||||
const publicacionIdsEnLote = new Set<number>();
|
||||
|
||||
if (items.length === 0) {
|
||||
currentErrors.general = "Debe agregar al menos una publicación.";
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const salidaNum = parseInt(item.cantSalida, 10);
|
||||
const entradaNum = parseInt(item.cantEntrada, 10);
|
||||
@@ -199,9 +238,8 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
const pubIdNum = Number(item.idPublicacion);
|
||||
if (publicacionIdsEnLote.has(pubIdNum)) {
|
||||
currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`;
|
||||
} else {
|
||||
publicacionIdsEnLote.add(pubIdNum);
|
||||
}
|
||||
} else { publicacionIdsEnLote.add(pubIdNum); }
|
||||
|
||||
if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) {
|
||||
currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`;
|
||||
}
|
||||
@@ -213,18 +251,11 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true;
|
||||
}
|
||||
});
|
||||
|
||||
const allItemsAreEmptyAndNoPubSelected = items.every(
|
||||
itm => itm.idPublicacion === '' &&
|
||||
(itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) &&
|
||||
(itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) &&
|
||||
itm.observacion.trim() === ''
|
||||
);
|
||||
|
||||
if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) {
|
||||
const allItemsAreEmptyAndNoPubSelected = items.every(itm => itm.idPublicacion === '' && (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && itm.observacion.trim() === '');
|
||||
if (!hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) {
|
||||
currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones.";
|
||||
} else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0)) && !allItemsAreEmptyAndNoPubSelected) {
|
||||
currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos.";
|
||||
} else if (items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida, 10) > 0 || parseInt(i.cantEntrada, 10) > 0 || i.observacion.trim() !== '')) && !allItemsAreEmptyAndNoPubSelected) {
|
||||
currentErrors.general = "Debe ingresar datos significativos (cantidades u observación) para al menos una publicación seleccionada.";
|
||||
}
|
||||
}
|
||||
setLocalErrors(currentErrors);
|
||||
@@ -232,6 +263,7 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
// ... (sin cambios)
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (parentErrorMessage) clearErrorMessage();
|
||||
if (modalSpecificApiError) setModalSpecificApiError(null);
|
||||
@@ -252,16 +284,17 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
cantEntrada: entradaNum,
|
||||
observacion: editObservacion.trim() || undefined,
|
||||
};
|
||||
// Aquí se llama al onSubmit que viene de la página padre (GestionarEntradasSalidasCanillaPage)
|
||||
// para la lógica de actualización.
|
||||
await onSubmit(dataToSubmitSingle, initialData.idParte);
|
||||
onClose(); // Cerrar el modal DESPUÉS de un submit de edición exitoso
|
||||
} else {
|
||||
// Lógica de creación BULK (se maneja internamente en el modal)
|
||||
await onSubmitEdit(dataToSubmitSingle, initialData.idParte);
|
||||
} else { // Modo Creación
|
||||
if (!displayIdCanilla || !displayFecha) {
|
||||
setModalSpecificApiError("Faltan datos del canillita o la fecha para crear los movimientos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items
|
||||
.filter(item =>
|
||||
item.idPublicacion && Number(item.idPublicacion) > 0 &&
|
||||
((parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '')
|
||||
( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida, 10) > 0 || parseInt(item.cantEntrada, 10) > 0) || item.observacion.trim() !== '' )
|
||||
)
|
||||
.map(item => ({
|
||||
idPublicacion: Number(item.idPublicacion),
|
||||
@@ -271,37 +304,30 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
}));
|
||||
|
||||
if (itemsToSubmit.length === 0) {
|
||||
setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar..." }));
|
||||
setLocalErrors(prev => ({ ...prev, general: "No hay movimientos válidos para registrar." }));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const bulkData: CreateBulkEntradaSalidaCanillaDto = {
|
||||
idCanilla: Number(idCanilla),
|
||||
fecha,
|
||||
idCanilla: Number(displayIdCanilla), // Usar el displayIdCanilla
|
||||
fecha: displayFecha, // Usar el displayFecha
|
||||
items: itemsToSubmit,
|
||||
};
|
||||
await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData);
|
||||
onClose(); // Cerrar el modal DESPUÉS de un submit de creación bulk exitoso
|
||||
}
|
||||
// onClose(); // Movido dentro de los bloques if/else para asegurar que solo se llama tras éxito
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de EntradaSalidaCanillaFormModal:", error);
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
setModalSpecificApiError(error.response.data?.message || 'Error al procesar la solicitud.');
|
||||
} else {
|
||||
setModalSpecificApiError('Ocurrió un error inesperado.');
|
||||
}
|
||||
// NO llamar a onClose() aquí si hubo un error, para que el modal permanezca abierto
|
||||
// y muestre el modalSpecificApiError.
|
||||
// Si la edición (que usa el 'onSubmit' del padre) lanza un error, ese error se propagará
|
||||
// al padre y el padre decidirá si el modal se cierra o no (actualmente no lo cierra).
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message
|
||||
? error.response.data.message
|
||||
: 'Ocurrió un error inesperado al procesar la solicitud.';
|
||||
setModalSpecificApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
const handleAddRow = () => { /* ... (sin cambios) ... */
|
||||
if (items.length >= publicaciones.length) {
|
||||
setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." }));
|
||||
return;
|
||||
@@ -309,15 +335,13 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]);
|
||||
setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
};
|
||||
|
||||
const handleRemoveRow = (idToRemove: string) => {
|
||||
const handleRemoveRow = (idToRemove: string) => { /* ... (sin cambios) ... */
|
||||
if (items.length <= 1 && !isEditing) return;
|
||||
setItems(items.filter(item => item.id !== idToRemove));
|
||||
};
|
||||
|
||||
const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => {
|
||||
setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); // CORREGIDO: item a itemRow para evitar conflicto de nombres de variable con el `item` del map en el JSX
|
||||
if (localErrors[`item_${id}_${field}`]) { // Aquí item se refiere al id del item.
|
||||
const handleItemChange = (id: string, field: keyof Omit<FormRowItem, 'id'>, value: string | number) => { /* ... (sin cambios) ... */
|
||||
setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow));
|
||||
if (localErrors[`item_${id}_${field}`]) {
|
||||
setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null }));
|
||||
}
|
||||
if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null }));
|
||||
@@ -325,200 +349,102 @@ const EntradaSalidaCanillaFormModal: React.FC<EntradaSalidaCanillaFormModalProps
|
||||
if (modalSpecificApiError) setModalSpecificApiError(null);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Movimiento Canillita' : 'Registrar Movimientos Canillita'}
|
||||
{isEditing ? `Editar Movimiento (ID: ${initialData?.idParte})` : 'Registrar Nuevos Movimientos'}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idCanilla} required>
|
||||
<InputLabel id="canilla-esc-select-label">Canillita</InputLabel>
|
||||
<Select labelId="canilla-esc-select-label" label="Canillita" value={idCanilla}
|
||||
onChange={(e) => { setIdCanilla(e.target.value as number); handleInputChange('idCanilla'); }}
|
||||
disabled={loading || loadingDropdowns || isEditing}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un canillita</em></MenuItem>
|
||||
{canillitas.map((c) => (<MenuItem key={c.idCanilla} value={c.idCanilla}>{`${c.nomApe} (Leg: ${c.legajo || 'S/L'})`}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idCanilla && <FormHelperText>{localErrors.idCanilla}</FormHelperText>}
|
||||
</FormControl>
|
||||
{/* --- MOSTRAR DATOS PRELLENADOS/FIJOS --- */}
|
||||
<Paper variant="outlined" sx={{ p: 1.5, mb: 2, backgroundColor: 'grey.100' }}>
|
||||
<Typography variant="body1" component="div" sx={{ display: 'flex', justifyContent: 'space-between', flexWrap:'wrap' }}>
|
||||
<Box><strong>{isEditing ? "Canillita:" : "Para Canillita:"}</strong> {displayNombreCanilla || 'N/A'}</Box>
|
||||
<Box><strong>{isEditing ? "Fecha Movimiento:" : "Para Fecha:"}</strong> {displayFecha ? new Date(displayFecha + 'T00:00:00Z').toLocaleDateString('es-AR', {timeZone: 'UTC'}) : 'N/A'}</Box>
|
||||
</Typography>
|
||||
{localErrors.idCanilla && <Typography color="error" variant="caption" display="block">{localErrors.idCanilla}</Typography>}
|
||||
{localErrors.fecha && <Typography color="error" variant="caption" display="block">{localErrors.fecha}</Typography>}
|
||||
</Paper>
|
||||
{/* --- FIN DATOS PRELLENADOS --- */}
|
||||
|
||||
<TextField label="Fecha Movimientos" type="date" value={fecha} required
|
||||
onChange={(e) => { setFecha(e.target.value); handleInputChange('fecha'); }}
|
||||
margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} InputLabelProps={{ shrink: true }}
|
||||
autoFocus={!isEditing && !idCanilla} // AutoFocus si es nuevo y no hay canillita seleccionado
|
||||
/>
|
||||
{/* El Select de Canillita y TextField de Fecha se eliminan de aquí si son fijos */}
|
||||
|
||||
{isEditing && initialData && (
|
||||
<Paper elevation={1} sx={{ p: 1.5, mt: 1 }}>
|
||||
<Typography variant="body2" gutterBottom color="text.secondary">Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 0.5 }}>
|
||||
<TextField label="Cant. Salida" type="number" value={editCantSalida}
|
||||
onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }}
|
||||
margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField label="Cant. Entrada" type="number" value={editCantEntrada}
|
||||
onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }}
|
||||
margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
<TextField label="Observación (General)" value={editObservacion}
|
||||
onChange={(e) => setEditObservacion(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }}
|
||||
{isEditing && initialData && (
|
||||
<Paper elevation={1} sx={{ p: 1.5, mt: 1 }}>
|
||||
<Typography variant="body2" gutterBottom color="text.secondary">
|
||||
Editando para Publicación: {publicaciones.find(p => p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, mt: 0.5, flexWrap: 'wrap' }}>
|
||||
<TextField label="Cant. Salida" type="number" value={editCantSalida}
|
||||
onChange={(e) => { setEditCantSalida(e.target.value); handleInputChange('editCantSalida'); }}
|
||||
margin="dense" error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
<TextField label="Cant. Entrada" type="number" value={editCantEntrada}
|
||||
onChange={(e) => { setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada'); }}
|
||||
margin="dense" error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''}
|
||||
disabled={loading} inputProps={{ min: 0 }} sx={{ flex: 1, minWidth:'120px' }}
|
||||
/>
|
||||
</Box>
|
||||
<TextField label="Observación (Movimiento)" value={editObservacion} // Label cambiado
|
||||
onChange={(e) => setEditObservacion(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{ mt: 1 }}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!isEditing && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography>
|
||||
{/* Indicador de carga para los items */}
|
||||
{loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>}
|
||||
{!loadingItems && items.map((itemRow, index) => (
|
||||
<Paper
|
||||
key={itemRow.id}
|
||||
elevation={1}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{/* Nivel 1: contenedor “padre” sin wrap */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center', // centra ícono + campos
|
||||
gap: 1,
|
||||
// NOTA: aquí NO ponemos flexWrap, por defecto es 'nowrap'
|
||||
}}
|
||||
>
|
||||
{/* Nivel 2: contenedor que agrupa solo los campos y sí puede hacer wrap */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap', // los campos sí hacen wrap si no caben
|
||||
flexGrow: 1, // ocupa todo el espacio disponible antes del ícono
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }}
|
||||
size="small"
|
||||
error={!!localErrors[`item_${itemRow.id}_idPublicacion`]}
|
||||
>
|
||||
<InputLabel
|
||||
required={
|
||||
parseInt(itemRow.cantSalida) > 0 ||
|
||||
parseInt(itemRow.cantEntrada) > 0 ||
|
||||
itemRow.observacion.trim() !== ''
|
||||
}
|
||||
>
|
||||
{!isEditing && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mt: 1.5, mb: 0.5 }}>Movimientos por Publicación:</Typography>
|
||||
{loadingItems && <Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}><CircularProgress size={20} /></Box>}
|
||||
{!loadingItems && items.map((itemRow, index) => (
|
||||
// ... (Renderizado de la fila de items sin cambios significativos,
|
||||
// solo asegúrate que el Select de Publicación use `publicaciones` y no `canillitas`)
|
||||
<Paper key={itemRow.id} elevation={1} sx={{ p: 1.5, mb: 1, }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', flexGrow: 1, }}>
|
||||
<FormControl sx={{ minWidth: 160, flexBasis: 'calc(40% - 8px)', flexGrow: 1, minHeight: 0 }} size="small" error={!!localErrors[`item_${itemRow.id}_idPublicacion`]} >
|
||||
<InputLabel required={ parseInt(itemRow.cantSalida) > 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== '' } >
|
||||
Pub. {index + 1}
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={itemRow.idPublicacion}
|
||||
label={`Publicación ${index + 1}`}
|
||||
onChange={(e) =>
|
||||
handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)
|
||||
}
|
||||
disabled={loading || loadingDropdowns}
|
||||
sx={{ minWidth: 0 }} // permite que shrink si hace falta
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
<em>Seleccione</em>
|
||||
</MenuItem>
|
||||
{publicaciones.map((p) => (
|
||||
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>
|
||||
{p.nombre}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Select value={itemRow.idPublicacion} label={`Publicación ${index + 1}`}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'idPublicacion', e.target.value as number)}
|
||||
disabled={loading || loadingDropdowns} sx={{ minWidth: 0 }} >
|
||||
<MenuItem value="" disabled> <em>Seleccione</em> </MenuItem>
|
||||
{publicaciones.map((p) => ( <MenuItem key={p.idPublicacion} value={p.idPublicacion}> {p.nombre} </MenuItem> ))}
|
||||
</Select>
|
||||
{localErrors[`item_${itemRow.id}_idPublicacion`] && (
|
||||
<FormHelperText>
|
||||
{localErrors[`item_${itemRow.id}_idPublicacion`]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
{localErrors[`item_${itemRow.id}_idPublicacion`] && ( <FormHelperText> {localErrors[`item_${itemRow.id}_idPublicacion`]} </FormHelperText> )}
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Llevados"
|
||||
type="number"
|
||||
size="small"
|
||||
value={itemRow.cantSalida}
|
||||
<TextField label="Llevados" type="number" size="small" value={itemRow.cantSalida}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'cantSalida', e.target.value)}
|
||||
error={!!localErrors[`item_${itemRow.id}_cantSalida`]}
|
||||
helperText={localErrors[`item_${itemRow.id}_cantSalida`]}
|
||||
inputProps={{ min: 0 }}
|
||||
sx={{
|
||||
flexBasis: 'calc(15% - 8px)',
|
||||
minWidth: '80px',
|
||||
minHeight: 0,
|
||||
}}
|
||||
error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]}
|
||||
inputProps={{ min: 0 }} sx={{ flexBasis: 'calc(15% - 8px)', minWidth: '80px', minHeight: 0, }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Devueltos"
|
||||
type="number"
|
||||
size="small"
|
||||
value={itemRow.cantEntrada}
|
||||
<TextField label="Devueltos" type="number" size="small" value={itemRow.cantEntrada}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'cantEntrada', e.target.value)}
|
||||
error={!!localErrors[`item_${itemRow.id}_cantEntrada`]}
|
||||
helperText={localErrors[`item_${itemRow.id}_cantEntrada`]}
|
||||
inputProps={{ min: 0 }}
|
||||
sx={{
|
||||
flexBasis: 'calc(15% - 8px)',
|
||||
minWidth: '80px',
|
||||
minHeight: 0,
|
||||
}}
|
||||
error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]}
|
||||
inputProps={{ min: 0 }} sx={{ flexBasis: 'calc(15% - 8px)', minWidth: '80px', minHeight: 0, }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Obs."
|
||||
value={itemRow.observacion}
|
||||
onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)}
|
||||
size="small"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
flexBasis: 'calc(25% - 8px)',
|
||||
minWidth: '120px',
|
||||
minHeight: 0,
|
||||
}}
|
||||
multiline
|
||||
maxRows={1}
|
||||
<TextField label="Obs." value={itemRow.observacion} onChange={(e) => handleItemChange(itemRow.id, 'observacion', e.target.value)}
|
||||
size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px', minHeight: 0, }}
|
||||
multiline maxRows={1}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Ícono de eliminar: siempre en la misma línea */}
|
||||
{items.length > 1 && (
|
||||
<IconButton
|
||||
onClick={() => handleRemoveRow(itemRow.id)}
|
||||
color="error"
|
||||
aria-label="Quitar fila"
|
||||
sx={{
|
||||
alignSelf: 'center', // mantén centrado verticalmente
|
||||
// No necesita flexShrink, porque el padre no hace wrap
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => handleRemoveRow(itemRow.id)} color="error" aria-label="Quitar fila" sx={{ alignSelf: 'center', }} >
|
||||
<DeleteIcon fontSize="medium" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
|
||||
{localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
|
||||
<Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}>
|
||||
Agregar Publicación
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{localErrors.general && <Alert severity="error" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
|
||||
<Button onClick={handleAddRow} startIcon={<AddIcon />} sx={{ mt: 1, alignSelf: 'flex-start' }} disabled={loading || loadingDropdowns || items.length >= publicaciones.length}>
|
||||
Agregar Publicación
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{parentErrorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{parentErrorMessage}</Alert>}
|
||||
{modalSpecificApiError && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{modalSpecificApiError}</Alert>}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import type { NovedadCanillaDto } from '../../../models/dtos/Distribucion/NovedadCanillaDto';
|
||||
import type { CreateNovedadCanillaDto } from '../../../models/dtos/Distribucion/CreateNovedadCanillaDto';
|
||||
import type { UpdateNovedadCanillaDto } from '../../../models/dtos/Distribucion/UpdateNovedadCanillaDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 500 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
interface NovedadCanillaFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => Promise<void>;
|
||||
// Props para pasar datos necesarios:
|
||||
idCanilla: number | null; // Necesario para crear una nueva novedad
|
||||
nombreCanilla?: string; // Para mostrar en el título
|
||||
initialData?: NovedadCanillaDto | null; // Para editar
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const NovedadCanillaFormModal: React.FC<NovedadCanillaFormModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
idCanilla,
|
||||
nombreCanilla,
|
||||
initialData,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [fecha, setFecha] = useState<string>('');
|
||||
const [detalle, setDetalle] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const isEditing = Boolean(initialData && initialData.idNovedad);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFecha(initialData?.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]);
|
||||
setDetalle(initialData?.detalle || '');
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, initialData, clearErrorMessage]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).';
|
||||
|
||||
if (!detalle.trim()) errors.detalle = 'El detalle es obligatorio.';
|
||||
else if (detalle.trim().length > 250) errors.detalle = 'El detalle no puede exceder los 250 caracteres.';
|
||||
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: 'fecha' | 'detalle') => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isEditing && initialData) {
|
||||
const dataToSubmit: UpdateNovedadCanillaDto = { detalle };
|
||||
await onSubmit(dataToSubmit, initialData.idNovedad);
|
||||
} else if (idCanilla) { // Asegurarse que idCanilla esté disponible para creación
|
||||
const dataToSubmit: CreateNovedadCanillaDto = {
|
||||
idCanilla: idCanilla, // Tomado de props
|
||||
fecha,
|
||||
detalle,
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
} else {
|
||||
// Esto no debería pasar si la lógica de la página que llama al modal es correcta
|
||||
setLocalErrors(prev => ({...prev, general: "No se proporcionó ID de Canillita para crear la novedad."}))
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
// El error de API es manejado por la página padre a través de 'errorMessage'
|
||||
console.error("Error en submit de NovedadCanillaFormModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
{isEditing ? 'Editar Novedad' : `Agregar Novedad para ${nombreCanilla || 'Canillita'}`}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{isEditing && initialData ? `Editando Novedad ID: ${initialData.idNovedad}` : `Canillita ID: ${idCanilla || 'N/A'}`}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
label="Fecha Novedad"
|
||||
type="date"
|
||||
value={fecha}
|
||||
required
|
||||
onChange={(e) => {setFecha(e.target.value); handleInputChange('fecha');}}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
error={!!localErrors.fecha}
|
||||
helperText={localErrors.fecha || ''}
|
||||
disabled={loading || isEditing} // Fecha no se edita
|
||||
InputLabelProps={{ shrink: true }}
|
||||
autoFocus={!isEditing}
|
||||
/>
|
||||
<TextField
|
||||
label="Detalle Novedad"
|
||||
value={detalle}
|
||||
required
|
||||
onChange={(e) => {setDetalle(e.target.value); handleInputChange('detalle');}}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
error={!!localErrors.detalle}
|
||||
helperText={localErrors.detalle || (detalle ? `${250 - detalle.length} caracteres restantes` : '')}
|
||||
disabled={loading}
|
||||
inputProps={{ maxLength: 250 }}
|
||||
/>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.general && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.general}</Alert>}
|
||||
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : (isEditing ? 'Guardar Cambios' : 'Agregar Novedad')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovedadCanillaFormModal;
|
||||
@@ -1,67 +1,225 @@
|
||||
import React from 'react';
|
||||
import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper } from '@mui/material'; // Quitar Grid
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper, Divider, TextField } from '@mui/material';
|
||||
import type { PermisoAsignadoDto } from '../../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
|
||||
interface PermisosChecklistProps {
|
||||
permisosDisponibles: PermisoAsignadoDto[];
|
||||
permisosSeleccionados: Set<number>;
|
||||
onPermisoChange: (permisoId: number, asignado: boolean) => void;
|
||||
onPermisoChange: (permisoId: number, asignado: boolean, esPermisoSeccion?: boolean, moduloHijo?: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
|
||||
// Mapeo de codAcc de sección a su módulo conceptual
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS002") return "Contables";
|
||||
if (codAcc === "SS003") return "Impresión";
|
||||
if (codAcc === "SS004") return "Reportes";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
if (codAcc === "SS006") return "Usuarios";
|
||||
return null;
|
||||
};
|
||||
|
||||
// Función para determinar el módulo conceptual de un permiso individual
|
||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
const moduloLower = permisoModulo.toLowerCase();
|
||||
|
||||
if (moduloLower.includes("distribuidores") ||
|
||||
moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas"
|
||||
moduloLower.includes("publicaciones distribución") ||
|
||||
moduloLower.includes("zonas distribuidores") ||
|
||||
moduloLower.includes("movimientos distribuidores") ||
|
||||
moduloLower.includes("empresas") || // Módulo "Empresas"
|
||||
moduloLower.includes("otros destinos") || // Cubre "Otros Destinos" y "Salidas Otros Destinos"
|
||||
moduloLower.includes("ctrl. devoluciones")) {
|
||||
return "Distribución";
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
return "Contables";
|
||||
}
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas"
|
||||
moduloLower.includes("impresión plantas") ||
|
||||
moduloLower.includes("tipos bobinas")) { // Añadido explícitamente
|
||||
return "Impresión";
|
||||
}
|
||||
if (moduloLower.includes("radios")) { // Asumiendo que los permisos de radios tendrán "Radios" en su módulo
|
||||
return "Radios";
|
||||
}
|
||||
if (moduloLower.includes("usuarios") || // Cubre "Usuarios" y "Perfiles"
|
||||
moduloLower.includes("perfiles")) {
|
||||
return "Usuarios";
|
||||
}
|
||||
if (moduloLower.includes("reportes")) { // Para los permisos RRxxx
|
||||
return "Reportes";
|
||||
}
|
||||
if (moduloLower.includes("permisos")) { // Para "Permisos (Definición)"
|
||||
return "Permisos (Definición)";
|
||||
}
|
||||
return permisoModulo; // Fallback al nombre original si no coincide
|
||||
};
|
||||
|
||||
|
||||
const PermisosChecklist: React.FC<PermisosChecklistProps> = ({
|
||||
permisosDisponibles,
|
||||
permisosSeleccionados,
|
||||
onPermisoChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const permisosAgrupados = permisosDisponibles.reduce((acc, permiso) => {
|
||||
const modulo = permiso.modulo || 'Otros';
|
||||
if (!acc[modulo]) {
|
||||
acc[modulo] = [];
|
||||
const [filtrosModulo, setFiltrosModulo] = useState<Record<string, string>>({});
|
||||
|
||||
const handleFiltroChange = (moduloConceptual: string, texto: string) => {
|
||||
setFiltrosModulo(prev => ({ ...prev, [moduloConceptual]: texto.toLowerCase() }));
|
||||
};
|
||||
|
||||
const permisosDeSeccion = permisosDisponibles.filter(p => p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX));
|
||||
const permisosNormales = permisosDisponibles.filter(p => !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX));
|
||||
|
||||
const permisosAgrupadosConceptualmente = permisosNormales.reduce((acc, permiso) => {
|
||||
const moduloConceptual = getModuloConceptualDelPermiso(permiso.modulo);
|
||||
if (!acc[moduloConceptual]) {
|
||||
acc[moduloConceptual] = [];
|
||||
}
|
||||
acc[modulo].push(permiso);
|
||||
acc[moduloConceptual].push(permiso);
|
||||
return acc;
|
||||
}, {} as Record<string, PermisoAsignadoDto[]>);
|
||||
|
||||
const ordenModulosPrincipales = ["Distribución", "Contables", "Impresión", "Radios", "Usuarios", "Reportes", "Permisos (Definición)"];
|
||||
// Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún)
|
||||
permisosDeSeccion.forEach(ps => {
|
||||
const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc);
|
||||
if (moduloConceptual && !ordenModulosPrincipales.includes(moduloConceptual)) {
|
||||
// Insertar después de un módulo conocido o al final si no hay un orden específico para él
|
||||
const indexReportes = ordenModulosPrincipales.indexOf("Reportes");
|
||||
if (indexReportes !== -1) {
|
||||
ordenModulosPrincipales.splice(indexReportes, 0, moduloConceptual);
|
||||
} else {
|
||||
ordenModulosPrincipales.push(moduloConceptual);
|
||||
}
|
||||
}
|
||||
if (moduloConceptual && !permisosAgrupadosConceptualmente[moduloConceptual]) {
|
||||
permisosAgrupadosConceptualmente[moduloConceptual] = []; // Asegurar que el grupo exista para Radios
|
||||
}
|
||||
});
|
||||
// Eliminar duplicados del orden por si acaso
|
||||
const ordenFinalModulos = [...new Set(ordenModulosPrincipales)];
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> {/* Contenedor Flexbox */}
|
||||
{Object.entries(permisosAgrupados).map(([modulo, permisosDelModulo]) => (
|
||||
<Box
|
||||
key={modulo}
|
||||
sx={{
|
||||
flexGrow: 1, // Para que las columnas crezcan
|
||||
flexBasis: { xs: '100%', sm: 'calc(50% - 16px)', md: 'calc(33.333% - 16px)' }, // Simula xs, sm, md
|
||||
// El '-16px' es por el gap (si el gap es 2 = 16px). Ajustar si el gap es diferente.
|
||||
// Alternativamente, usar porcentajes más simples y dejar que el flexWrap maneje el layout.
|
||||
// flexBasis: '300px', // Un ancho base y dejar que flexWrap haga el resto
|
||||
minWidth: '280px', // Ancho mínimo para cada columna
|
||||
maxWidth: { xs: '100%', sm: '50%', md: '33.333%' }, // Máximo ancho
|
||||
}}
|
||||
>
|
||||
<Paper elevation={2} sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle1" gutterBottom component="div" sx={{ fontWeight: 'bold', borderBottom: 1, borderColor: 'divider', pb: 1, mb:1 }}>
|
||||
{modulo}
|
||||
</Typography>
|
||||
<FormGroup sx={{ flexGrow: 1}}> {/* Para que ocupe el espacio vertical */}
|
||||
{permisosDelModulo.map((permiso) => (
|
||||
<FormControlLabel
|
||||
key={permiso.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={permisosSeleccionados.has(permiso.id)}
|
||||
onChange={(e) => onPermisoChange(permiso.id, e.target.checked)}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">{`${permiso.descPermiso} (${permiso.codAcc})`}</Typography>}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2.5, justifyContent: 'center' }}>
|
||||
{ordenFinalModulos.map(moduloConceptual => { // Usar ordenFinalModulos
|
||||
const permisoSeccionAsociado = permisosDeSeccion.find(
|
||||
ps => getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptual
|
||||
);
|
||||
const permisosDelModuloHijosOriginales = permisosAgrupadosConceptualmente[moduloConceptual] || [];
|
||||
|
||||
// Condición para renderizar la sección
|
||||
if (!permisoSeccionAsociado && permisosDelModuloHijosOriginales.length === 0 && moduloConceptual !== "Permisos (Definición)") {
|
||||
// No renderizar si no hay permiso de sección Y no hay hijos, EXCEPTO para "Permisos (Definición)" que es especial
|
||||
return null;
|
||||
}
|
||||
if (moduloConceptual === "Permisos (Definición)" && permisosDelModuloHijosOriginales.length === 0) {
|
||||
// No renderizar "Permisos (Definición)" si no tiene hijos
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const esSeccionSeleccionada = permisoSeccionAsociado ? permisosSeleccionados.has(permisoSeccionAsociado.id) : false;
|
||||
const todosHijosSeleccionados = permisosDelModuloHijosOriginales.length > 0 && permisosDelModuloHijosOriginales.every(p => permisosSeleccionados.has(p.id));
|
||||
const ningunHijoSeleccionado = permisosDelModuloHijosOriginales.every(p => !permisosSeleccionados.has(p.id));
|
||||
const algunosHijosSeleccionados = !todosHijosSeleccionados && !ningunHijoSeleccionado && permisosDelModuloHijosOriginales.length > 0;
|
||||
|
||||
const textoFiltro = filtrosModulo[moduloConceptual] || '';
|
||||
const permisosDelModuloHijosFiltrados = textoFiltro
|
||||
? permisosDelModuloHijosOriginales.filter(permiso =>
|
||||
permiso.descPermiso.toLowerCase().includes(textoFiltro) ||
|
||||
permiso.codAcc.toLowerCase().includes(textoFiltro)
|
||||
)
|
||||
: permisosDelModuloHijosOriginales;
|
||||
|
||||
return (
|
||||
<Box key={moduloConceptual} sx={{ /* ... estilos del Box ... */
|
||||
flexGrow: 1,
|
||||
flexBasis: { xs: '100%', sm: 'calc(50% - 20px)', md: 'calc(33.333% - 20px)' },
|
||||
minWidth: '320px', // Aumentar un poco para el filtro
|
||||
maxWidth: { xs: '100%', sm: 'calc(50% - 10px)', md: 'calc(33.333% - 10px)'},
|
||||
}}>
|
||||
<Paper elevation={2} sx={{ p: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle1" gutterBottom component="div" sx={{ fontWeight: 'bold', borderBottom: 1, borderColor: 'divider', pb: 1, mb: 1 }}>
|
||||
{moduloConceptual}
|
||||
</Typography>
|
||||
|
||||
{permisosDelModuloHijosOriginales.length > 3 && ( // Mostrar filtro si hay más de 3 permisos
|
||||
<TextField
|
||||
label={`Buscar en ${moduloConceptual}...`}
|
||||
variant="standard"
|
||||
size="small"
|
||||
fullWidth
|
||||
value={filtrosModulo[moduloConceptual] || ''}
|
||||
onChange={(e) => handleFiltroChange(moduloConceptual, e.target.value)}
|
||||
sx={{ mb: 1, mt: 0.5 }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
)}
|
||||
|
||||
{permisoSeccionAsociado && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
label={`Acceso a Sección ${moduloConceptual}`} // Cambiado el Label
|
||||
labelPlacement="end"
|
||||
sx={{mb:1, '& .MuiFormControlLabel-label': { fontWeight: 'medium'}}}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={esSeccionSeleccionada && (permisosDelModuloHijosOriginales.length === 0 || todosHijosSeleccionados)}
|
||||
indeterminate={esSeccionSeleccionada && (algunosHijosSeleccionados || (ningunHijoSeleccionado && permisosDelModuloHijosOriginales.length > 0))}
|
||||
onChange={() => onPermisoChange(permisoSeccionAsociado.id, false, true, moduloConceptual)}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Mostrar Divider solo si hay hijos o es la sección Radios (que queremos que aparezca aunque esté vacía) */}
|
||||
{(permisosDelModuloHijosOriginales.length > 0 || moduloConceptual === "Radios") && <Divider sx={{mb:1.5}}/> }
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ maxHeight: '280px', overflowY: 'auto', flexGrow: 1 }}> {/* Aumentar un poco maxHeight */}
|
||||
<FormGroup sx={{ pl: permisoSeccionAsociado ? 2 : 0 }}>
|
||||
{permisosDelModuloHijosFiltrados.map((permiso) => (
|
||||
<FormControlLabel
|
||||
key={permiso.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={permisosSeleccionados.has(permiso.id)}
|
||||
onChange={(e) => onPermisoChange(permiso.id, e.target.checked, false, moduloConceptual)}
|
||||
disabled={disabled || (permisoSeccionAsociado && !esSeccionSeleccionada)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">{`${permiso.descPermiso} (${permiso.codAcc})`}</Typography>}
|
||||
/>
|
||||
))}
|
||||
{textoFiltro && permisosDelModuloHijosFiltrados.length === 0 && permisosDelModuloHijosOriginales.length > 0 && (
|
||||
<Typography variant="caption" sx={{p:1, fontStyle: 'italic', textAlign: 'center'}}>
|
||||
No hay permisos que coincidan con "{textoFiltro}".
|
||||
</Typography>
|
||||
)}
|
||||
{/* Mensaje si no hay hijos en general (y no es por filtro) */}
|
||||
{permisosDelModuloHijosOriginales.length === 0 && !textoFiltro && moduloConceptual !== "Permisos (Definición)" && (
|
||||
<Typography variant="caption" sx={{p:1, fontStyle: 'italic', textAlign: 'center'}}>
|
||||
No hay permisos específicos en esta sección.
|
||||
</Typography>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
import React, { type ReactNode, useState, useEffect } from 'react';
|
||||
// src/layouts/MainLayout.tsx
|
||||
import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo
|
||||
from 'react';
|
||||
import {
|
||||
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
|
||||
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider // Nuevas importaciones
|
||||
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle'; // Icono de usuario
|
||||
import LockResetIcon from '@mui/icons-material/LockReset'; // Icono para cambiar contraseña
|
||||
import LogoutIcon from '@mui/icons-material/Logout'; // Icono para cerrar sesión
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||
import LockResetIcon from '@mui/icons-material/LockReset';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { usePermissions } from '../hooks/usePermissions'; // <<--- AÑADIR ESTA LÍNEA
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const modules = [
|
||||
{ label: 'Inicio', path: '/' },
|
||||
{ label: 'Distribución', path: '/distribucion' },
|
||||
{ label: 'Contables', path: '/contables' },
|
||||
{ label: 'Impresión', path: '/impresion' },
|
||||
{ label: 'Reportes', path: '/reportes' },
|
||||
{ label: 'Radios', path: '/radios' },
|
||||
{ label: 'Usuarios', path: '/usuarios' },
|
||||
// Definición original de módulos
|
||||
const allAppModules = [
|
||||
{ label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible
|
||||
{ label: 'Distribución', path: '/distribucion', requiredPermission: 'SS001' },
|
||||
{ label: 'Contables', path: '/contables', requiredPermission: 'SS002' },
|
||||
{ label: 'Impresión', path: '/impresion', requiredPermission: 'SS003' },
|
||||
{ label: 'Reportes', path: '/reportes', requiredPermission: 'SS004' },
|
||||
{ label: 'Radios', path: '/radios', requiredPermission: 'SS005' },
|
||||
{ label: 'Usuarios', path: '/usuarios', requiredPermission: 'SS006' },
|
||||
];
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
const {
|
||||
user,
|
||||
user, // user ya está disponible aquí
|
||||
logout,
|
||||
isAuthenticated,
|
||||
isPasswordChangeForced,
|
||||
@@ -35,24 +40,40 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
passwordChangeCompleted
|
||||
} = useAuth();
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions(); // <<--- OBTENER HOOK DE PERMISOS
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
||||
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); // Estado para el menú de usuario
|
||||
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null);
|
||||
|
||||
// --- INICIO DE CAMBIO: Filtrar módulos basados en permisos ---
|
||||
const accessibleModules = useMemo(() => {
|
||||
if (!isAuthenticated) return []; // Si no está autenticado, ningún módulo excepto quizás login (que no está aquí)
|
||||
return allAppModules.filter(module => {
|
||||
if (module.requiredPermission === null) return true; // Inicio siempre accesible
|
||||
return isSuperAdmin || tienePermiso(module.requiredPermission);
|
||||
});
|
||||
}, [isAuthenticated, isSuperAdmin, tienePermiso]);
|
||||
// --- FIN DE CAMBIO ---
|
||||
|
||||
useEffect(() => {
|
||||
const currentModulePath = modules.findIndex(module =>
|
||||
// --- INICIO DE CAMBIO: Usar accessibleModules para encontrar el tab ---
|
||||
const currentModulePath = accessibleModules.findIndex(module =>
|
||||
location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/'))
|
||||
);
|
||||
if (currentModulePath !== -1) {
|
||||
setSelectedTab(currentModulePath);
|
||||
} else if (location.pathname === '/') {
|
||||
setSelectedTab(0); // Asegurar que la pestaña de Inicio se seleccione para la ruta raíz
|
||||
// Asegurar que Inicio se seleccione si es accesible
|
||||
const inicioIndex = accessibleModules.findIndex(m => m.path === '/');
|
||||
if (inicioIndex !== -1) setSelectedTab(inicioIndex);
|
||||
else setSelectedTab(false);
|
||||
} else {
|
||||
setSelectedTab(false); // Ninguna pestaña seleccionada si no coincide
|
||||
setSelectedTab(false);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
// --- FIN DE CAMBIO ---
|
||||
}, [location.pathname, accessibleModules]); // << CAMBIO: dependencia a accessibleModules
|
||||
|
||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElUserMenu(event.currentTarget);
|
||||
@@ -69,7 +90,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
|
||||
const handleLogoutClick = () => {
|
||||
logout();
|
||||
handleCloseUserMenu(); // Cierra el menú antes de desloguear completamente
|
||||
handleCloseUserMenu();
|
||||
};
|
||||
|
||||
const handleModalClose = (passwordChangedSuccessfully: boolean) => {
|
||||
@@ -77,23 +98,27 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
passwordChangeCompleted();
|
||||
} else {
|
||||
if (isPasswordChangeForced) {
|
||||
logout();
|
||||
logout(); // Si es forzado y cancela/falla, desloguear
|
||||
} else {
|
||||
setShowForcedPasswordChangeModal(false);
|
||||
setShowForcedPasswordChangeModal(false); // Si no es forzado, solo cerrar modal
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setSelectedTab(newValue);
|
||||
navigate(modules[newValue].path);
|
||||
// --- INICIO DE CAMBIO: Navegar usando accessibleModules ---
|
||||
if (accessibleModules[newValue]) {
|
||||
setSelectedTab(newValue);
|
||||
navigate(accessibleModules[newValue].path);
|
||||
}
|
||||
// --- FIN DE CAMBIO ---
|
||||
};
|
||||
|
||||
// Determinar si el módulo actual es el de Reportes
|
||||
const isReportesModule = location.pathname.startsWith('/reportes');
|
||||
|
||||
if (showForcedPasswordChangeModal && isPasswordChangeForced) {
|
||||
return (
|
||||
// ... (sin cambios)
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
||||
<ChangePasswordModal
|
||||
open={showForcedPasswordChangeModal}
|
||||
@@ -104,17 +129,31 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Si no hay módulos accesibles después del login (y no es el cambio de clave forzado)
|
||||
// Esto podría pasar si un usuario no tiene permiso para NINGUNA sección, ni siquiera Inicio.
|
||||
// Deberías redirigir a login o mostrar un mensaje de "Sin acceso".
|
||||
if (isAuthenticated && !isPasswordChangeForced && accessibleModules.length === 0) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
||||
<Typography variant="h6">No tiene acceso a ninguna sección del sistema.</Typography>
|
||||
<Button onClick={logout} sx={{ mt: 2 }}>Cerrar Sesión</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<AppBar position="sticky" elevation={1} /* Elevation sutil para AppBar */>
|
||||
<AppBar position="sticky" elevation={1}>
|
||||
<Toolbar sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6" component="div" noWrap sx={{ cursor: 'pointer' }} onClick={() => navigate('/')}>
|
||||
Sistema de Gestión - El Día
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{user && (
|
||||
<Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} /* Ocultar en pantallas muy pequeñas */>
|
||||
{/* ... (Menú de usuario sin cambios) ... */}
|
||||
{user && (
|
||||
<Typography sx={{ mr: 2, display: { xs: 'none', sm: 'block' } }} >
|
||||
Hola, {user.nombreCompleto}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -125,9 +164,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
aria-label="Cuenta del usuario"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
sx={{
|
||||
padding: '15px',
|
||||
}}
|
||||
sx={{ padding: '15px' }}
|
||||
onClick={handleOpenUserMenu}
|
||||
color="inherit"
|
||||
>
|
||||
@@ -143,15 +180,14 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
onClose={handleCloseUserMenu}
|
||||
sx={{ '& .MuiPaper-root': { minWidth: 220, marginTop: '8px' } }}
|
||||
>
|
||||
{user && ( // Mostrar info del usuario en el menú
|
||||
<Box sx={{ px: 2, py: 1.5, pointerEvents: 'none' /* Para que no sea clickeable */ }}>
|
||||
{user && (
|
||||
<Box sx={{ px: 2, py: 1.5, pointerEvents: 'none' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>{user.nombreCompleto}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">{user.username}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{user && <Divider sx={{ mb: 1 }} />}
|
||||
|
||||
{!isPasswordChangeForced && ( // No mostrar si ya está forzado a cambiarla
|
||||
{!isPasswordChangeForced && (
|
||||
<MenuItem onClick={handleChangePasswordClick}>
|
||||
<ListItemIcon><LockResetIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Cambiar Contraseña</ListItemText>
|
||||
@@ -166,48 +202,45 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
)}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
<Paper square elevation={0} >
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onChange={handleTabChange}
|
||||
indicatorColor="secondary" // O 'primary' si prefieres el mismo color que el fondo
|
||||
textColor="inherit" // El texto de la pestaña hereda el color (blanco sobre fondo oscuro)
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
aria-label="módulos principales"
|
||||
sx={{
|
||||
backgroundColor: 'primary.main', // Color de fondo de las pestañas
|
||||
color: 'white', // Color del texto de las pestañas
|
||||
'& .MuiTabs-indicator': {
|
||||
height: 3, // Un indicador un poco más grueso
|
||||
},
|
||||
'& .MuiTab-root': { // Estilo para cada pestaña
|
||||
minWidth: 100, // Ancho mínimo para cada pestaña
|
||||
textTransform: 'none', // Evitar MAYÚSCULAS por defecto
|
||||
fontWeight: 'normal',
|
||||
opacity: 0.85, // Ligeramente transparentes si no están seleccionadas
|
||||
'&.Mui-selected': {
|
||||
fontWeight: 'bold',
|
||||
opacity: 1,
|
||||
// color: 'secondary.main' // Opcional: color diferente para la pestaña seleccionada
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
{modules.map((module) => (
|
||||
<Tab key={module.path} label={module.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
{/* --- INICIO DE CAMBIO: Renderizar Tabs solo si hay módulos accesibles y está autenticado --- */}
|
||||
{isAuthenticated && accessibleModules.length > 0 && (
|
||||
<Paper square elevation={0} >
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onChange={handleTabChange}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
aria-label="módulos principales"
|
||||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'white',
|
||||
'& .MuiTabs-indicator': { height: 3 },
|
||||
'& .MuiTab-root': {
|
||||
minWidth: 100, textTransform: 'none',
|
||||
fontWeight: 'normal', opacity: 0.85,
|
||||
'&.Mui-selected': { fontWeight: 'bold', opacity: 1 },
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Mapear sobre accessibleModules en lugar de allAppModules */}
|
||||
{accessibleModules.map((module) => (
|
||||
<Tab key={module.path} label={module.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
)}
|
||||
{/* --- FIN DE CAMBIO --- */}
|
||||
</AppBar>
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
sx={{ /* ... (estilos sin cambios) ... */
|
||||
flexGrow: 1,
|
||||
py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding vertical responsivo
|
||||
px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 }, // Padding horizontal responsivo
|
||||
py: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 },
|
||||
px: isReportesModule ? 0 : { xs: 1.5, sm: 2, md: 2.5 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
@@ -215,17 +248,19 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
<Box component="footer" sx={{ p: 1, backgroundColor: 'grey.200' /* Un gris más claro */, color: 'text.secondary', textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}>
|
||||
<Box component="footer" sx={{ /* ... (estilos sin cambios) ... */
|
||||
p: 1, backgroundColor: 'grey.200', color: 'text.secondary',
|
||||
textAlign: 'left', borderTop: (theme) => `1px solid ${theme.palette.divider}`
|
||||
}}>
|
||||
<Typography variant="caption">
|
||||
{/* Puedes usar caption para un texto más pequeño en el footer */}
|
||||
Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Administrador' : (user?.perfil || `ID ${user?.idPerfil}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<ChangePasswordModal
|
||||
open={showForcedPasswordChangeModal && !isPasswordChangeForced} // Solo mostrar si no es el forzado inicial
|
||||
onClose={() => handleModalClose(false)} // Asumir que si se cierra sin cambiar, no fue exitoso
|
||||
isFirstLogin={false} // Este modal no es para el primer login forzado
|
||||
open={showForcedPasswordChangeModal && !isPasswordChangeForced}
|
||||
onClose={() => handleModalClose(false)}
|
||||
isFirstLogin={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface AjusteSaldoRequestDto {
|
||||
destino: 'Distribuidores' | 'Canillas';
|
||||
idDestino: number;
|
||||
idEmpresa: number;
|
||||
montoAjuste: number;
|
||||
justificacion: string;
|
||||
}
|
||||
10
Frontend/src/models/dtos/Contables/SaldoGestionDto.ts
Normal file
10
Frontend/src/models/dtos/Contables/SaldoGestionDto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface SaldoGestionDto {
|
||||
idSaldo: number;
|
||||
destino: string; // "Distribuidores" o "Canillas"
|
||||
idDestino: number;
|
||||
nombreDestinatario: string;
|
||||
idEmpresa: number;
|
||||
nombreEmpresa: string;
|
||||
monto: number;
|
||||
fechaUltimaModificacion: string; // "yyyy-MM-ddTHH:mm:ss" o similar
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface CreateNovedadCanillaDto {
|
||||
idCanilla: number;
|
||||
fecha: string; // string dd/MM/yyyy
|
||||
detalle?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DistribuidorDropdownDto {
|
||||
idDistribuidor: number;
|
||||
nombre: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DistribuidorLookupDto {
|
||||
idDistribuidor: number;
|
||||
nombre: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface EmpresaDropdownDto {
|
||||
idEmpresa: number;
|
||||
nombre: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface EmpresaLookupDto {
|
||||
idEmpresa: number;
|
||||
nombre: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface NovedadCanillaDto {
|
||||
idNovedad: number;
|
||||
idCanilla: number;
|
||||
nombreCanilla: string;
|
||||
fecha: string; // string dd/MM/yyyy
|
||||
detalle?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface UpdateNovedadCanillaDto {
|
||||
detalle?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface CanillaGananciaReporteDto {
|
||||
canilla: string; // NomApe del canillita
|
||||
legajo?: number | null;
|
||||
francos?: number | null;
|
||||
faltas?: number | null;
|
||||
totalRendir?: number | null;
|
||||
id?: string; // Para el DataGrid
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface ListadoDistCanMensualDiariosDto {
|
||||
canilla: string;
|
||||
elDia: number | null;
|
||||
elPlata: number | null;
|
||||
vendidos: number | null;
|
||||
importeElDia: number | null;
|
||||
importeElPlata: number | null;
|
||||
importeTotal: number | null;
|
||||
id?: string; // Para DataGrid
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ListadoDistCanMensualPubDto {
|
||||
publicacion: string;
|
||||
canilla: string;
|
||||
totalCantSalida: number | null;
|
||||
totalCantEntrada: number | null;
|
||||
totalRendir: number | null;
|
||||
id?: string; // Para DataGrid
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface NovedadesCanillasReporteDto {
|
||||
nomApe: string;
|
||||
fecha: string;
|
||||
detalle?: string | null;
|
||||
id?: string; // Para el DataGrid
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
const contablesSubModules = [
|
||||
{ label: 'Pagos Distribuidores', path: 'pagos-distribuidores' },
|
||||
{ label: 'Notas Crédito/Débito', path: 'notas-cd' },
|
||||
{ label: 'Gestión de Saldos', path: 'gestion-saldos' },
|
||||
{ label: 'Tipos de Pago', path: 'tipos-pago' },
|
||||
];
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ const GestionarNotasCDPage: React.FC = () => {
|
||||
const [selectedRow, setSelectedRow] = useState<NotaCreditoDebitoDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// CN001 (Ver), CN002 (Crear), CN003 (Modificar), CN004 (Eliminar)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CN001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CN002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CN003");
|
||||
|
||||
257
Frontend/src/pages/Contables/GestionarSaldosPage.tsx
Normal file
257
Frontend/src/pages/Contables/GestionarSaldosPage.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, IconButton, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select
|
||||
} from '@mui/material';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote'; // Icono para ajustar saldo
|
||||
|
||||
import saldoService from '../../services/Contables/saldoService';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
|
||||
import type { SaldoGestionDto } from '../../models/dtos/Contables/SaldoGestionDto';
|
||||
import type { AjusteSaldoRequestDto } from '../../models/dtos/Contables/AjusteSaldoRequestDto';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
|
||||
import AjusteSaldoModal from '../../components/Modals/Contables/AjusteSaldoModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
type TipoDestinoFiltro = 'Distribuidores' | 'Canillas' | '';
|
||||
|
||||
const GestionarSaldosPage: React.FC = () => {
|
||||
const [saldos, setSaldos] = useState<SaldoGestionDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para modal
|
||||
|
||||
// Filtros
|
||||
const [filtroTipoDestino, setFiltroTipoDestino] = useState<TipoDestinoFiltro>('');
|
||||
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<(DistribuidorDto | CanillaDto)[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalAjusteOpen, setModalAjusteOpen] = useState(false);
|
||||
const [saldoParaAjustar, setSaldoParaAjustar] = useState<SaldoGestionDto | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
// No necesitamos menú de acciones por fila si el ajuste es la única acción
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerSaldos = isSuperAdmin || tienePermiso("CS001"); // Permiso para ver
|
||||
const puedeAjustarSaldos = isSuperAdmin || tienePermiso("CS002"); // Permiso para ajustar
|
||||
|
||||
|
||||
const fetchDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
setError(null);
|
||||
try {
|
||||
const empData = await empresaService.getAllEmpresas();
|
||||
setEmpresas(empData);
|
||||
|
||||
if (filtroTipoDestino === 'Distribuidores') {
|
||||
const distData = await distribuidorService.getAllDistribuidores();
|
||||
setDestinatariosDropdown(distData);
|
||||
} else if (filtroTipoDestino === 'Canillas') {
|
||||
const canData = await canillaService.getAllCanillas(undefined, undefined, true); // Solo activos
|
||||
setDestinatariosDropdown(canData);
|
||||
} else {
|
||||
setDestinatariosDropdown([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos para filtros:", err);
|
||||
setError("Error al cargar opciones de filtro.");
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false);
|
||||
}
|
||||
}, [filtroTipoDestino]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDropdownData();
|
||||
}, [fetchDropdownData]);
|
||||
|
||||
|
||||
const cargarSaldos = useCallback(async () => {
|
||||
if (!puedeVerSaldos) {
|
||||
setError("No tiene permiso para ver saldos."); setLoading(false); return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const params = {
|
||||
destino: filtroTipoDestino || undefined, // Enviar undefined si está vacío
|
||||
idDestino: filtroIdDestino ? Number(filtroIdDestino) : undefined,
|
||||
idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined,
|
||||
};
|
||||
const data = await saldoService.getAllSaldosGestion(params);
|
||||
setSaldos(data);
|
||||
} catch (err) {
|
||||
console.error("Error al cargar saldos:", err);
|
||||
setError('Error al cargar los saldos.');
|
||||
setSaldos([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVerSaldos, filtroTipoDestino, filtroIdDestino, filtroIdEmpresa]);
|
||||
|
||||
useEffect(() => {
|
||||
cargarSaldos();
|
||||
}, [cargarSaldos]);
|
||||
|
||||
|
||||
const handleOpenAjusteModal = (saldo: SaldoGestionDto) => {
|
||||
if (!puedeAjustarSaldos) {
|
||||
setApiErrorMessage("No tiene permiso para ajustar saldos.");
|
||||
return;
|
||||
}
|
||||
setSaldoParaAjustar(saldo);
|
||||
setApiErrorMessage(null);
|
||||
setModalAjusteOpen(true);
|
||||
};
|
||||
const handleCloseAjusteModal = () => {
|
||||
setModalAjusteOpen(false);
|
||||
setSaldoParaAjustar(null);
|
||||
};
|
||||
|
||||
const handleSubmitAjusteModal = async (data: AjusteSaldoRequestDto) => {
|
||||
if (!puedeAjustarSaldos) return;
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await saldoService.ajustarSaldo(data);
|
||||
cargarSaldos(); // Recargar lista para ver el saldo actualizado
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al aplicar el ajuste de saldo.';
|
||||
setApiErrorMessage(message);
|
||||
throw err; // Para que el modal sepa que hubo error
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
|
||||
const displayData = saldos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleString('es-AR', {timeZone:'UTC'}) : '-';
|
||||
const formatCurrency = (value: number) => value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' });
|
||||
|
||||
|
||||
if (!loading && !puedeVerSaldos) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado a esta sección."}</Alert></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestión de Saldos</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: 180, flexGrow: 1 }}>
|
||||
<InputLabel>Tipo Destinatario</InputLabel>
|
||||
<Select value={filtroTipoDestino} label="Tipo Destinatario"
|
||||
onChange={(e) => {
|
||||
setFiltroTipoDestino(e.target.value as TipoDestinoFiltro);
|
||||
setFiltroIdDestino(''); // Resetear destinatario al cambiar tipo
|
||||
}}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
<MenuItem value="Distribuidores">Distribuidores</MenuItem>
|
||||
<MenuItem value="Canillas">Canillitas</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingFiltersDropdown || !filtroTipoDestino}>
|
||||
<InputLabel>Destinatario Específico</InputLabel>
|
||||
<Select value={filtroIdDestino} label="Destinatario Específico"
|
||||
onChange={(e) => setFiltroIdDestino(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{destinatariosDropdown.map(d => (
|
||||
<MenuItem key={'idDistribuidor' in d ? d.idDistribuidor : d.idCanilla} value={'idDistribuidor' in d ? d.idDistribuidor : d.idCanilla}>
|
||||
{'nomApe' in d ? d.nomApe : d.nombre}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Empresa</InputLabel>
|
||||
<Select value={filtroIdEmpresa} label="Empresa"
|
||||
onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{/* No hay botón de "Agregar Saldo", se crean automáticamente */}
|
||||
</Paper>
|
||||
|
||||
{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>}
|
||||
|
||||
{!loading && !error && puedeVerSaldos && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Destinatario</TableCell>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Tipo</TableCell>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Empresa</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight:'bold'}}>Monto Saldo</TableCell>
|
||||
<TableCell sx={{fontWeight:'bold'}}>Últ. Modificación</TableCell>
|
||||
{puedeAjustarSaldos && <TableCell align="right" sx={{fontWeight:'bold'}}>Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeAjustarSaldos ? 6 : 5} align="center">No se encontraron saldos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((s) => (
|
||||
<TableRow key={s.idSaldo} hover
|
||||
sx={{ backgroundColor: s.monto < 0 ? 'rgba(255, 0, 0, 0.05)' : (s.monto > 0 ? 'rgba(0, 255, 0, 0.05)' : 'inherit')}}
|
||||
>
|
||||
<TableCell>{s.nombreDestinatario}</TableCell>
|
||||
<TableCell>{s.destino}</TableCell>
|
||||
<TableCell>{s.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right" sx={{fontWeight:500}}>{formatCurrency(s.monto)}</TableCell>
|
||||
<TableCell>{formatDate(s.fechaUltimaModificacion)}</TableCell>
|
||||
{puedeAjustarSaldos && (
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => handleOpenAjusteModal(s)} color="primary">
|
||||
<EditNoteIcon fontSize="small"/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={saldos.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{saldoParaAjustar &&
|
||||
<AjusteSaldoModal
|
||||
open={modalAjusteOpen}
|
||||
onClose={handleCloseAjusteModal}
|
||||
onSubmit={handleSubmitAjusteModal}
|
||||
saldoParaAjustar={saldoParaAjustar}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarSaldosPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/configuracion/GestionarTiposPagoPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
@@ -7,10 +6,10 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add'; // Icono para agregar
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Icono para más opciones
|
||||
import EditIcon from '@mui/icons-material/Edit'; // Icono para modificar
|
||||
import DeleteIcon from '@mui/icons-material/Delete'; // Icono para eliminar
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import tipoPagoService from '../../services/Contables/tipoPagoService';
|
||||
import type { TipoPago } from '../../models/Entities/TipoPago';
|
||||
import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto';
|
||||
@@ -30,20 +29,26 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25); // Cambiado a un valor más común
|
||||
|
||||
// Para el menú contextual de cada fila
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedTipoPagoRow, setSelectedTipoPagoRow] = useState<TipoPago | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions(); // Obtener también isSuperAdmin
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CT001"); // << AÑADIR ESTA LÍNEA
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CT002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CT003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("CT004");
|
||||
|
||||
|
||||
const cargarTiposPago = useCallback(async () => {
|
||||
if (!puedeVer) { // << AÑADIR CHEQUEO DE PERMISO AQUÍ
|
||||
setError("No tiene permiso para ver los tipos de pago.");
|
||||
setLoading(false);
|
||||
setTiposPago([]); // Asegurar que no se muestren datos previos
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -55,7 +60,7 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filtroNombre]);
|
||||
}, [filtroNombre, puedeVer]); // << AÑADIR puedeVer A LAS DEPENDENCIAS
|
||||
|
||||
useEffect(() => {
|
||||
cargarTiposPago();
|
||||
@@ -73,15 +78,15 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateTipoPagoDto | UpdateTipoPagoDto) => {
|
||||
setApiErrorMessage(null); // Limpiar error previo
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingTipoPago && 'idTipoPago' in data) { // Es Update
|
||||
if (editingTipoPago && editingTipoPago.idTipoPago) {
|
||||
await tipoPagoService.updateTipoPago(editingTipoPago.idTipoPago, data as UpdateTipoPagoDto);
|
||||
} else { // Es Create
|
||||
} else {
|
||||
await tipoPagoService.createTipoPago(data as CreateTipoPagoDto);
|
||||
}
|
||||
cargarTiposPago(); // Recargar lista
|
||||
// onClose se llama desde el modal en caso de éxito
|
||||
cargarTiposPago();
|
||||
// onClose se llama desde el modal si todo va bien
|
||||
} catch (err: any) {
|
||||
console.error("Error en submit modal (padre):", err);
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
@@ -89,11 +94,12 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
} else {
|
||||
setApiErrorMessage('Ocurrió un error inesperado al guardar.');
|
||||
}
|
||||
throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
// ... (sin cambios)
|
||||
if (window.confirm('¿Está seguro de que desea eliminar este tipo de pago?')) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
@@ -126,12 +132,24 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 25));
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); // << CORREGIDO: base 10, no 25
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
// Renderizado condicional si no tiene permiso para ver
|
||||
if (!loading && !puedeVer) { // << AÑADIR ESTE BLOQUE
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Tipos de Pago
|
||||
</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
@@ -146,10 +164,8 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
size="small"
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
// sx={{ flexGrow: 1 }} // Opcional, para que ocupe más espacio
|
||||
disabled={!puedeVer || loading} // Deshabilitar si no puede ver o está cargando
|
||||
/>
|
||||
{/* El botón de búsqueda se activa al cambiar el texto, pero puedes añadir uno explícito */}
|
||||
{/* <Button variant="contained" onClick={cargarTiposPago}>Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
@@ -164,43 +180,50 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{/* Mostrar error de carga si no es un error de "sin permiso" y no hay error de API */}
|
||||
{error && !loading && puedeVer && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
|
||||
{!loading && !error && (
|
||||
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true para mostrar la tabla
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nombre</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
{/* Mostrar columna de acciones solo si tiene algún permiso de acción */}
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={3} align="center">No se encontraron tipos de pago.</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeEliminar) ? 3 : 2} align="center">
|
||||
No se encontraron tipos de pago.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayData.map((tipo) => (
|
||||
<TableRow key={tipo.idTipoPago}>
|
||||
<TableCell>{tipo.nombre}</TableCell>
|
||||
<TableCell>{tipo.detalle || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, tipo)}
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, tipo)}
|
||||
disabled={!puedeModificar && !puedeEliminar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]}
|
||||
rowsPerPageOptions={[25, 50, 100]} // Opciones más estándar
|
||||
component="div"
|
||||
count={tiposPago.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
@@ -217,20 +240,19 @@ const GestionarTiposPagoPage: React.FC = () => {
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
{puedeModificar && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow!); handleMenuClose(); }}>
|
||||
{puedeModificar && selectedTipoPagoRow && ( // Asegurar que selectedTipoPagoRow no sea null
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedTipoPagoRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && (
|
||||
<MenuItem onClick={() => handleDelete(selectedTipoPagoRow!.idTipoPago)}>
|
||||
{puedeEliminar && selectedTipoPagoRow && ( // Asegurar que selectedTipoPagoRow no sea null
|
||||
<MenuItem onClick={() => handleDelete(selectedTipoPagoRow.idTipoPago)}>
|
||||
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Si no tiene ningún permiso, el menú podría estar vacío o no mostrarse */}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
{selectedTipoPagoRow && (!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<TipoPagoFormModal
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/distribucion/DistribucionIndexPage.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, Chip, FormControlLabel, ListItemIcon, ListItemText // << AÑADIR ListItemIcon, ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote'; // << AÑADIR IMPORTACIÓN DEL ICONO
|
||||
import { useNavigate } from 'react-router-dom'; // << AÑADIR IMPORTACIÓN DE useNavigate
|
||||
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
||||
@@ -31,17 +34,24 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25); // << CAMBIADO DE 5 a 25 (valor más común)
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedCanillitaRow, setSelectedCanillitaRow] = useState<CanillaDto | null>(null);
|
||||
|
||||
const navigate = useNavigate(); // << INICIALIZAR useNavigate
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CG001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CG002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CG003");
|
||||
// CG004 para Porcentajes/Montos, se gestionará por separado.
|
||||
const puedeDarBaja = isSuperAdmin || tienePermiso("CG005");
|
||||
// Permisos para Novedades
|
||||
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006"); // << DEFINIR PERMISO
|
||||
// Para la opción "Ver Novedades", podemos usar el permiso de ver canillitas (CG001)
|
||||
// O si solo se quiere mostrar si puede gestionarlas, usar puedeGestionarNovedades
|
||||
const puedeVerNovedadesCanilla = puedeVer || puedeGestionarNovedades; // << LÓGICA PARA MOSTRAR LA OPCIÓN
|
||||
|
||||
|
||||
const cargarCanillitas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
@@ -51,12 +61,12 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 25) : undefined;
|
||||
const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined; // << CORREGIDO: parseInt con base 10
|
||||
if (filtroLegajo && isNaN(legajoNum!)) {
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]); // Limpiar resultados si el filtro es inválido
|
||||
setLoading(false);
|
||||
return;
|
||||
setApiErrorMessage("Legajo debe ser un número.");
|
||||
setCanillitas([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos);
|
||||
setCanillitas(data);
|
||||
@@ -83,6 +93,7 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
await canillaService.createCanilla(data as CreateCanillaDto);
|
||||
}
|
||||
cargarCanillitas();
|
||||
// No es necesario llamar a handleCloseModal aquí si el modal se cierra solo en éxito
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
@@ -93,17 +104,22 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = canillita.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) {
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err:any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
try {
|
||||
await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja });
|
||||
cargarCanillitas();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleOpenNovedades = (idCan: number) => {
|
||||
navigate(`/distribucion/canillas/${idCan}/novedades`);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, canillita: CanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita);
|
||||
};
|
||||
@@ -118,98 +134,120 @@ const GestionarCanillitasPage: React.FC = () => {
|
||||
const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso."}</Alert></Box>;
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert></Box>; // Mensaje más genérico
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Canillitas</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Filtrar por Nombre/Apellido"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number" // Mantener como number para el input, la conversión se hace al usarlo
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos} // Default a true
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
value={filtroNomApe}
|
||||
onChange={(e) => setFiltroNomApe(e.target.value)}
|
||||
sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre
|
||||
/>
|
||||
<TextField
|
||||
label="Filtrar por Legajo"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={filtroLegajo}
|
||||
onChange={(e) => setFiltroLegajo(e.target.value)}
|
||||
sx={{ flex: 1, minWidth: '150px' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Ver Activos"
|
||||
sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarCanillitas} size="small">Buscar</Button> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
/>
|
||||
}
|
||||
label="Ver Activos" // Cambiado el label para más claridad
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Canillita</Button>
|
||||
)}
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
{/* Mostrar error general si no hay error de API específico */}
|
||||
{error && !apiErrorMessage && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined"/> : <Chip label="No" color="default" size="small" variant="outlined"/>}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}>
|
||||
<MoreVertIcon />
|
||||
{!loading && !error && puedeVer && ( // Asegurar que puedeVer sea true también aquí
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Legajo</TableCell><TableCell>Nombre y Apellido</TableCell>
|
||||
<TableCell>Zona</TableCell><TableCell>Empresa</TableCell>
|
||||
<TableCell>Accionista</TableCell><TableCell>Estado</TableCell>
|
||||
{/* Mostrar acciones solo si tiene algún permiso para el menú */}
|
||||
{(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) ? 7 : 6} align="center">No se encontraron canillitas.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((c) => (
|
||||
<TableRow key={c.idCanilla} hover sx={{ backgroundColor: c.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{c.legajo || '-'}</TableCell><TableCell>{c.nomApe}</TableCell>
|
||||
<TableCell>{c.nombreZona}</TableCell><TableCell>{c.empresa === 0 ? '-' : c.nombreEmpresa}</TableCell>
|
||||
<TableCell>{c.accionista ? <Chip label="Sí" color="success" size="small" variant="outlined" /> : <Chip label="No" color="default" size="small" variant="outlined" />}</TableCell>
|
||||
<TableCell>{c.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
{(puedeModificar || puedeDarBaja || puedeVerNovedadesCanilla) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, c)}
|
||||
// Deshabilitar si NO tiene NINGUNO de los permisos para las acciones del menú
|
||||
disabled={!puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={canillitas.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
{selectedCanillitaRow.baja ? <ToggleOnIcon sx={{mr:1}}/> : <ToggleOffIcon sx={{mr:1}}/>}
|
||||
{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}
|
||||
</MenuItem>
|
||||
{/* Mostrar opción de Novedades si tiene permiso de ver canillitas o gestionar novedades */}
|
||||
{puedeVerNovedadesCanilla && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleOpenNovedades(selectedCanillitaRow.idCanilla)}>
|
||||
<ListItemIcon><EventNoteIcon /></ListItemIcon>
|
||||
<ListItemText>Novedades</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeModificar && selectedCanillitaRow && ( // Asegurar que selectedCanillitaRow existe
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedCanillitaRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeDarBaja && selectedCanillitaRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedCanillitaRow)}>
|
||||
<ListItemIcon>{selectedCanillitaRow.baja ? <ToggleOnIcon /> : <ToggleOffIcon />}</ListItemIcon>
|
||||
<ListItemText>{selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Mostrar "Sin acciones" si no hay ninguna acción permitida para la fila seleccionada */}
|
||||
{selectedCanillitaRow && !puedeModificar && !puedeDarBaja && !puedeVerNovedadesCanilla && (
|
||||
<MenuItem disabled>Sin acciones</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeDarBaja) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
<CanillaFormModal
|
||||
|
||||
@@ -146,8 +146,8 @@ const GestionarEmpresasPage: React.FC = () => {
|
||||
// Si no tiene permiso para ver, mostrar mensaje y salir
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Empresas</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Empresas</Typography>
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, Checkbox, Tooltip,
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
|
||||
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle,
|
||||
ToggleButtonGroup, ToggleButton
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import PrintIcon from '@mui/icons-material/Print';
|
||||
@@ -19,7 +20,7 @@ import canillaService from '../../services/Distribucion/canillaService';
|
||||
|
||||
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
||||
|
||||
@@ -28,25 +29,32 @@ import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
|
||||
type TipoDestinatarioFiltro = 'canillitas' | 'accionistas';
|
||||
|
||||
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true); // Para carga principal de movimientos
|
||||
const [error, setError] = useState<string | null>(null); // Error general o de carga
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para errores de modal/API
|
||||
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
|
||||
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [filtroIdCanillitaSeleccionado, setFiltroIdCanillitaSeleccionado] = useState<number | string>('');
|
||||
const [filtroTipoDestinatario, setFiltroTipoDestinatario] = useState<TipoDestinatarioFiltro>('canillitas');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
|
||||
const [prefillModalData, setPrefillModalData] = useState<{
|
||||
fecha?: string;
|
||||
idCanilla?: number | string;
|
||||
nombreCanilla?: string; // << AÑADIDO PARA PASAR AL MODAL
|
||||
idPublicacion?: number | string;
|
||||
} | null>(null);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
@@ -64,70 +72,123 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const puedeLiquidar = isSuperAdmin || tienePermiso("MC005");
|
||||
const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006");
|
||||
|
||||
// Función para formatear fechas YYYY-MM-DD a DD/MM/YYYY
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
const datePart = dateString.split('T')[0];
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; }
|
||||
return datePart;
|
||||
};
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [pubsData, canData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
canillaService.getAllCanillas(undefined, undefined, true)
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setCanillitas(canData);
|
||||
} catch (err) {
|
||||
console.error(err); setError("Error al cargar opciones de filtro.");
|
||||
} finally { setLoadingFiltersDropdown(false); }
|
||||
useEffect(() => {
|
||||
const fetchPublicaciones = async () => {
|
||||
setLoadingFiltersDropdown(true); // Mover al inicio de la carga de pubs
|
||||
try {
|
||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicaciones(pubsData);
|
||||
} catch (err) {
|
||||
console.error("Error cargando publicaciones para filtro:",err);
|
||||
setError("Error al cargar publicaciones."); // Usar error general
|
||||
} finally {
|
||||
// No poner setLoadingFiltersDropdown(false) aquí, esperar a que ambas cargas terminen
|
||||
}
|
||||
};
|
||||
fetchPublicaciones();
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
||||
const fetchDestinatariosParaDropdown = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true); // Poner al inicio de esta carga también
|
||||
setFiltroIdCanillitaSeleccionado('');
|
||||
setDestinatariosDropdown([]);
|
||||
setError(null); // Limpiar errores de carga de dropdowns previos
|
||||
try {
|
||||
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
|
||||
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter);
|
||||
setDestinatariosDropdown(data);
|
||||
} catch (err) {
|
||||
console.error("Error cargando destinatarios para filtro:", err);
|
||||
setError("Error al cargar canillitas/accionistas."); // Usar error general
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false); // Poner al final de AMBAS cargas de dropdown
|
||||
}
|
||||
}, [filtroTipoDestinatario]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDestinatariosParaDropdown();
|
||||
}, [fetchDestinatariosParaDropdown]);
|
||||
|
||||
|
||||
const cargarMovimientos = useCallback(async () => {
|
||||
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
|
||||
if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; }
|
||||
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
let liquidadosFilter: boolean | null = null;
|
||||
let incluirNoLiquidadosFilter: boolean | null = true; // Por defecto mostrar no liquidados
|
||||
|
||||
if (filtroEstadoLiquidacion === 'liquidados') {
|
||||
liquidadosFilter = true;
|
||||
incluirNoLiquidadosFilter = false;
|
||||
} else if (filtroEstadoLiquidacion === 'noLiquidados') {
|
||||
liquidadosFilter = false;
|
||||
incluirNoLiquidadosFilter = true;
|
||||
} // Si es 'todos', ambos son null o true y false respectivamente (backend debe manejarlo)
|
||||
|
||||
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
|
||||
fechaDesde: filtroFecha,
|
||||
fechaHasta: filtroFecha,
|
||||
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
|
||||
idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null,
|
||||
liquidados: liquidadosFilter,
|
||||
incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter,
|
||||
idCanilla: Number(filtroIdCanillitaSeleccionado),
|
||||
liquidados: null,
|
||||
incluirNoLiquidados: null,
|
||||
};
|
||||
const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params);
|
||||
setMovimientos(data);
|
||||
setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar
|
||||
setSelectedIdsParaLiquidar(new Set());
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar movimientos.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]);
|
||||
console.error("Error al cargar movimientos:", err);
|
||||
setError('Error al cargar movimientos.');
|
||||
setMovimientos([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||
cargarMovimientos();
|
||||
} else {
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false); // Asegurar que no se quede en loading si los filtros se limpian
|
||||
}
|
||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); // `cargarMovimientos` ya tiene sus dependencias
|
||||
|
||||
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
|
||||
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
if (!puedeCrear && !item) {
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
}
|
||||
if (item && !puedeModificar) {
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
} else {
|
||||
// --- CAMBIO: Obtener nombre del canillita seleccionado para prefill ---
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe, // << AÑADIR NOMBRE
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// ... handleDelete, handleMenuOpen, handleMenuClose, handleSelectRowForLiquidar, handleSelectAllForLiquidar, handleOpenLiquidarDialog, handleCloseLiquidarDialog sin cambios ...
|
||||
const handleDelete = async (idParte: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
@@ -138,7 +199,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
||||
// Almacenar el idParte en el propio elemento del menú para referencia
|
||||
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedRow(item);
|
||||
@@ -154,7 +214,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
const handleSelectAllForLiquidar = (event: React.ChangeEvent<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 {
|
||||
@@ -170,64 +230,55 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
setOpenLiquidarDialog(true);
|
||||
};
|
||||
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
|
||||
|
||||
|
||||
const handleConfirmLiquidar = async () => {
|
||||
if (selectedIdsParaLiquidar.size === 0) {
|
||||
setApiErrorMessage("No hay movimientos seleccionados para liquidar.");
|
||||
return;
|
||||
}
|
||||
if (!fechaLiquidacionDialog) {
|
||||
setApiErrorMessage("Debe seleccionar una fecha de liquidación.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- VALIDACIÓN DE FECHA ---
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); // Usar Z para consistencia con formatDate si es necesario, o T00:00:00 para local
|
||||
|
||||
if (selectedIdsParaLiquidar.size === 0) { /* ... */ return; }
|
||||
if (!fechaLiquidacionDialog) { /* ... */ return; }
|
||||
// ... (validación de fecha sin cambios)
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
|
||||
let fechaMovimientoMasReciente: Date | null = null;
|
||||
|
||||
selectedIdsParaLiquidar.forEach(idParte => {
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) { // Asegurarse que movimiento.fecha existe
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); // Consistencia con Z
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
setApiErrorMessage(null);
|
||||
setLoading(true); // Usar el loading general para la operación de liquidar
|
||||
|
||||
setLoading(true);
|
||||
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
|
||||
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
|
||||
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
|
||||
fechaLiquidacion: fechaLiquidacionDialog
|
||||
};
|
||||
|
||||
try {
|
||||
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
||||
setOpenLiquidarDialog(false);
|
||||
|
||||
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
||||
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
|
||||
|
||||
await cargarMovimientos();
|
||||
|
||||
if (movimientoParaTicket) {
|
||||
console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
|
||||
// --- CAMBIO: NO IMPRIMIR TICKET SI ES ACCIONISTA ---
|
||||
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
fechaLiquidacionDialog,
|
||||
movimientoParaTicket.canillaEsAccionista
|
||||
false // esAccionista = false
|
||||
);
|
||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
|
||||
} else {
|
||||
console.warn("No se pudo encontrar información del movimiento para generar el ticket post-liquidación.");
|
||||
console.warn("No se pudo encontrar información del movimiento para ticket post-liquidación.");
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.';
|
||||
setApiErrorMessage(msg);
|
||||
@@ -236,8 +287,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Esta función se pasa al modal para que la invoque al hacer submit en MODO EDICIÓN
|
||||
const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => {
|
||||
// ... (sin cambios)
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
|
||||
@@ -251,32 +302,21 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditingMovimiento(null);
|
||||
// Recargar siempre que se cierre el modal y no haya un error pendiente a nivel de página
|
||||
// Opcionalmente, podrías tener una bandera ' cambiosGuardados' que el modal active
|
||||
// para ser más selectivo con la recarga.
|
||||
setPrefillModalData(null);
|
||||
if (!apiErrorMessage) {
|
||||
cargarMovimientos();
|
||||
}
|
||||
};
|
||||
|
||||
const handleImprimirTicketLiquidacion = useCallback(async (
|
||||
// Parámetros necesarios para el ticket
|
||||
idCanilla: number,
|
||||
fecha: string, // Fecha para la que se genera el ticket (probablemente fechaLiquidacionDialog)
|
||||
esAccionista: boolean
|
||||
idCanilla: number, fecha: string, esAccionista: boolean
|
||||
) => {
|
||||
// ... (sin cambios)
|
||||
setLoadingTicketPdf(true);
|
||||
setApiErrorMessage(null);
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
|
||||
idCanilla: idCanilla,
|
||||
esAccionista: esAccionista,
|
||||
};
|
||||
|
||||
const params = { fecha: fecha.split('T')[0], idCanilla, esAccionista };
|
||||
const blob = await reportesService.getTicketLiquidacionCanillaPdf(params);
|
||||
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar el ticket PDF.";
|
||||
@@ -287,16 +327,11 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
if (!w) alert("Permita popups para ver el PDF del ticket.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error al generar ticket de liquidación:", error);
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message
|
||||
? error.response.data.message
|
||||
: 'Ocurrió un error al generar el ticket.';
|
||||
console.error("Error al generar ticket:", error);
|
||||
const message = axios.isAxiosError(error) && error.response?.data?.message ? error.response.data.message : 'Error al generar ticket.';
|
||||
setApiErrorMessage(message);
|
||||
} finally {
|
||||
setLoadingTicketPdf(false);
|
||||
// No cerramos el menú aquí si se llama desde handleConfirmLiquidar
|
||||
}
|
||||
}, []); // Dependencias vacías si no usa nada del scope exterior que cambie, o añadir si es necesario
|
||||
} finally { setLoadingTicketPdf(false); }
|
||||
}, []);
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
@@ -305,47 +340,77 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { // Modificado para solo mostrar si no hay filtros y no puede ver
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
}
|
||||
|
||||
|
||||
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
|
||||
// Corregido: numNotLiquidatedOnPage debe calcularse sobre 'movimientos' filtrados, no solo 'displayData'
|
||||
// O, si la selección es solo por página, displayData está bien. Asumamos selección por página por ahora.
|
||||
const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</Typography>
|
||||
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas & Accionistas</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 }}>
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
||||
required
|
||||
error={!filtroFecha} // Se marca error si está vacío
|
||||
helperText={!filtroFecha ? "Fecha es obligatoria" : ""}
|
||||
/>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={filtroTipoDestinatario}
|
||||
exclusive
|
||||
onChange={(_, newValue: TipoDestinatarioFiltro | null) => {
|
||||
if (newValue !== null) {
|
||||
setFiltroTipoDestinatario(newValue);
|
||||
}
|
||||
}}
|
||||
aria-label="Tipo de Destinatario"
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="canillitas">Canillitas</ToggleButton>
|
||||
<ToggleButton value="accionistas">Accionistas</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingFiltersDropdown} required error={!filtroIdCanillitaSeleccionado}>
|
||||
<InputLabel>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</InputLabel>
|
||||
<Select
|
||||
value={filtroIdCanillitaSeleccionado}
|
||||
label={filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}
|
||||
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>)}
|
||||
</Select>
|
||||
{!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}>
|
||||
<InputLabel>Publicación</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
|
||||
<InputLabel>Publicación (Opcional)</InputLabel>
|
||||
<Select value={filtroIdPublicacion} label="Publicación (Opcional)" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Canillita</InputLabel>
|
||||
<Select value={filtroIdCanilla} label="Canillita" onChange={(e) => setFiltroIdCanilla(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{canillitas.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
|
||||
<InputLabel>Estado Liquidación</InputLabel>
|
||||
<Select value={filtroEstadoLiquidacion} label="Estado Liquidación" onChange={(e) => setFiltroEstadoLiquidacion(e.target.value as 'todos' | 'liquidados' | 'noLiquidados')}>
|
||||
<MenuItem value="noLiquidados">No Liquidados</MenuItem>
|
||||
<MenuItem value="liquidados">Liquidados</MenuItem>
|
||||
<MenuItem value="todos">Todos</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</Button>)}
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && (
|
||||
{/* --- CAMBIO: DESHABILITAR BOTÓN SI FILTROS OBLIGATORIOS NO ESTÁN --- */}
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado} // <<-- AÑADIDO
|
||||
>
|
||||
Registrar Movimiento
|
||||
</Button>
|
||||
)}
|
||||
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
|
||||
<Button variant="contained" color="success" startIcon={<PlaylistAddCheckIcon />} onClick={handleOpenLiquidarDialog}>
|
||||
Liquidar Seleccionados ({numSelectedToLiquidate})
|
||||
</Button>
|
||||
@@ -353,8 +418,12 @@ 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>}
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{/* Mostrar error general si no hay error de API específico y no está cargando filtros */}
|
||||
{error && !loading && !apiErrorMessage && !loadingFiltersDropdown && <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 }}>
|
||||
@@ -364,12 +433,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
|
||||
// ... (Tabla y Paginación sin cambios)
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
{puedeLiquidar && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
|
||||
@@ -381,7 +451,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
)}
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell>Publicación</TableCell>
|
||||
<TableCell>Canillita</TableCell>
|
||||
<TableCell>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</TableCell>
|
||||
<TableCell align="right">Salida</TableCell>
|
||||
<TableCell align="right">Entrada</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
@@ -397,19 +467,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
(puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
|
||||
9 +
|
||||
((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
(puedeLiquidar ? 1 : 0) + 9 + ((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
|
||||
}
|
||||
align="center"
|
||||
>
|
||||
No se encontraron movimientos.
|
||||
No se encontraron movimientos con los filtros aplicados.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayData.map((m) => (
|
||||
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
|
||||
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
|
||||
{puedeLiquidar && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={selectedIdsParaLiquidar.has(m.idParte)}
|
||||
@@ -440,8 +508,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={(e) => handleMenuOpen(e, m)}
|
||||
data-rowid={m.idParte.toString()} // Guardar el id de la fila aquí
|
||||
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} // Lógica simplificada, refinar si es necesario
|
||||
data-rowid={m.idParte.toString()}
|
||||
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
@@ -462,19 +530,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<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>)}
|
||||
|
||||
{/* Opción de Imprimir Ticket Liq. */}
|
||||
{selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir)
|
||||
{/* --- CAMBIO: MOSTRAR REIMPRIMIR TICKET SIEMPRE SI ESTÁ LIQUIDADO --- */}
|
||||
{selectedRow && selectedRow.liquidado && puedeLiquidar && ( // Usar puedeLiquidar para consistencia
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
|
||||
if (selectedRow) {
|
||||
handleImprimirTicketLiquidacion(
|
||||
selectedRow.idCanilla,
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
|
||||
selectedRow.canillaEsAccionista
|
||||
selectedRow.fechaLiquidado || selectedRow.fecha,
|
||||
selectedRow.canillaEsAccionista // Pasar si es accionista
|
||||
);
|
||||
}
|
||||
// handleMenuClose() es llamado por handleImprimirTicketLiquidacion
|
||||
}}
|
||||
disabled={loadingTicketPdf}
|
||||
>
|
||||
@@ -483,13 +549,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
Reimprimir Ticket Liq.
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{selectedRow && ( // Opción de Eliminar
|
||||
{selectedRow && (
|
||||
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
|
||||
) && (
|
||||
<MenuItem onClick={() => {
|
||||
if (selectedRow) handleDelete(selectedRow.idParte);
|
||||
}}>
|
||||
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -498,13 +561,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<EntradaSalidaCanillaFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleModalEditSubmit}
|
||||
onSubmit={handleModalEditSubmit} // Este onSubmit es solo para edición
|
||||
initialData={editingMovimiento}
|
||||
prefillData={prefillModalData}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
{/* ... (Dialog de Liquidación sin cambios) ... */}
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<DialogTitle>Confirmar Liquidación</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
@@ -523,7 +588,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// src/pages/Distribucion/GestionarNovedadesCanillaPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||
CircularProgress, Alert, TextField, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
import novedadCanillaService from '../../services/Distribucion/novedadCanillaService';
|
||||
import canillaService from '../../services/Distribucion/canillaService';
|
||||
import type { NovedadCanillaDto } from '../../models/dtos/Distribucion/NovedadCanillaDto';
|
||||
import type { CreateNovedadCanillaDto } from '../../models/dtos/Distribucion/CreateNovedadCanillaDto';
|
||||
import type { UpdateNovedadCanillaDto } from '../../models/dtos/Distribucion/UpdateNovedadCanillaDto';
|
||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
||||
import NovedadCanillaFormModal from '../../components/Modals/Distribucion/NovedadCanillaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarNovedadesCanillaPage: React.FC = () => {
|
||||
const { idCanilla: idCanillaStr } = useParams<{ idCanilla: string }>();
|
||||
const navigate = useNavigate();
|
||||
const idCanilla = Number(idCanillaStr);
|
||||
|
||||
const [canillita, setCanillita] = useState<CanillaDto | null>(null);
|
||||
const [novedades, setNovedades] = useState<NovedadCanillaDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [errorPage, setErrorPage] = useState<string | null>(null); // Error general de la página
|
||||
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>('');
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingNovedad, setEditingNovedad] = useState<NovedadCanillaDto | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para modal/delete
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedNovedadRow, setSelectedNovedadRow] = useState<NovedadCanillaDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeGestionarNovedades = isSuperAdmin || tienePermiso("CG006");
|
||||
const puedeVerCanillitas = isSuperAdmin || tienePermiso("CG001");
|
||||
|
||||
// Cargar datos del canillita (solo una vez o si idCanilla cambia)
|
||||
useEffect(() => {
|
||||
if (isNaN(idCanilla)) {
|
||||
setErrorPage("ID de Canillita inválido.");
|
||||
setLoading(false); // Detener carga principal
|
||||
return;
|
||||
}
|
||||
if (!puedeVerCanillitas && !puedeGestionarNovedades) {
|
||||
setErrorPage("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); // Iniciar carga para datos del canillita
|
||||
const fetchCanillita = async () => {
|
||||
try {
|
||||
if (puedeVerCanillitas) {
|
||||
const canData = await canillaService.getCanillaById(idCanilla);
|
||||
setCanillita(canData);
|
||||
} else {
|
||||
// Si no puede ver detalles del canillita pero sí novedades, al menos mostrar ID
|
||||
setCanillita({ idCanilla, nomApe: `ID ${idCanilla}` } as CanillaDto);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error cargando datos del canillita:", err);
|
||||
setErrorPage(`Error al cargar datos del canillita (ID: ${idCanilla}).`);
|
||||
}
|
||||
// No ponemos setLoading(false) aquí, porque la carga de novedades sigue.
|
||||
};
|
||||
fetchCanillita();
|
||||
}, [idCanilla, puedeVerCanillitas, puedeGestionarNovedades]);
|
||||
|
||||
|
||||
// Cargar/filtrar novedades
|
||||
const cargarNovedades = useCallback(async () => {
|
||||
if (isNaN(idCanilla) || (!puedeGestionarNovedades && !puedeVerCanillitas)) {
|
||||
// Los permisos ya se validaron en el useEffect anterior, pero es bueno tenerlo
|
||||
return;
|
||||
}
|
||||
// Si ya está cargando los datos del canillita, no iniciar otra carga paralela
|
||||
// Se usará el mismo 'loading' para ambas operaciones iniciales.
|
||||
// if (!loading) setLoading(true); // No es necesario si el useEffect anterior ya lo hizo
|
||||
|
||||
setApiErrorMessage(null); // Limpiar errores de API de acciones previas
|
||||
// setErrorPage(null); // No limpiar error de página aquí, podría ser por el canillita
|
||||
|
||||
try {
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null,
|
||||
fechaHasta: filtroFechaHasta || null,
|
||||
};
|
||||
const dataNovedades = await novedadCanillaService.getNovedadesPorCanilla(idCanilla, params);
|
||||
setNovedades(dataNovedades);
|
||||
// Si no hay datos con filtros, no es un error de API, simplemente no hay datos.
|
||||
// El mensaje de "no hay novedades" se maneja en la tabla.
|
||||
} catch (err: any) {
|
||||
console.error("Error al cargar/filtrar novedades:", err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al cargar las novedades.';
|
||||
setErrorPage(message); // Usar el error de página para problemas de carga de novedades
|
||||
setNovedades([]); // Limpiar en caso de error
|
||||
} finally {
|
||||
// Solo poner setLoading(false) después de que AMBAS cargas (canillita y novedades) se intenten.
|
||||
// Como se llaman en secuencia implícita por los useEffect, el último setLoading(false) es el de novedades.
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idCanilla, puedeGestionarNovedades, puedeVerCanillitas, filtroFechaDesde, filtroFechaHasta]);
|
||||
|
||||
// useEffect para cargar novedades cuando los filtros o el canillita (o permisos) cambian
|
||||
useEffect(() => {
|
||||
// Solo cargar si tenemos un idCanilla válido y permisos
|
||||
if (!isNaN(idCanilla) && (puedeGestionarNovedades || puedeVerCanillitas)) {
|
||||
cargarNovedades();
|
||||
} else if (isNaN(idCanilla)){
|
||||
setErrorPage("ID de Canillita inválido.");
|
||||
setLoading(false);
|
||||
} else if (!puedeGestionarNovedades && !puedeVerCanillitas) {
|
||||
setErrorPage("No tiene permiso para acceder a esta sección.");
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idCanilla, cargarNovedades, puedeGestionarNovedades, puedeVerCanillitas]); // `cargarNovedades` ya tiene sus dependencias
|
||||
|
||||
|
||||
const handleOpenModal = (item?: NovedadCanillaDto) => {
|
||||
if (!puedeGestionarNovedades) {
|
||||
setApiErrorMessage("No tiene permiso para agregar o editar novedades.");
|
||||
return;
|
||||
}
|
||||
setEditingNovedad(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingNovedad(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateNovedadCanillaDto | UpdateNovedadCanillaDto, idNovedad?: number) => {
|
||||
if (!puedeGestionarNovedades) return;
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
if (editingNovedad && idNovedad) {
|
||||
await novedadCanillaService.updateNovedad(idNovedad, data as UpdateNovedadCanillaDto);
|
||||
} else {
|
||||
await novedadCanillaService.createNovedad(data as CreateNovedadCanillaDto);
|
||||
}
|
||||
cargarNovedades(); // Recargar lista de novedades
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la novedad.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idNovedadDelRow: number) => {
|
||||
if (!puedeGestionarNovedades) return;
|
||||
if (window.confirm(`¿Seguro de eliminar esta novedad (ID: ${idNovedadDelRow})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await novedadCanillaService.deleteNovedad(idNovedadDelRow);
|
||||
cargarNovedades(); // Recargar lista de novedades
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la novedad.';
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: NovedadCanillaDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedNovedadRow(item);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedNovedadRow(null);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-';
|
||||
|
||||
|
||||
if (loading && !canillita) { // Muestra cargando solo si aún no tenemos los datos del canillita
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (errorPage && !canillita) { // Si hay un error al cargar el canillita, no mostrar nada más
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{errorPage}</Alert>;
|
||||
}
|
||||
|
||||
// Si no tiene permiso para la sección en general
|
||||
if (!puedeGestionarNovedades && !puedeVerCanillitas) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/canillas`)} sx={{ mb: 2 }}>
|
||||
Volver a Canillitas
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Novedades de: {canillita?.nomApe || `Canillita ID ${idCanilla}`}
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2}}>
|
||||
{puedeGestionarNovedades && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: {xs: 2, sm:0} }}>
|
||||
Agregar Novedad
|
||||
</Button>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<FilterListIcon sx={{color: 'action.active', alignSelf:'center'}} />
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde}
|
||||
onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }}
|
||||
disabled={loading} // Deshabilitar durante cualquier carga
|
||||
/>
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta}
|
||||
onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }}
|
||||
disabled={loading} // Deshabilitar durante cualquier carga
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Mostrar error de API (de submit/delete) o error de carga de novedades */}
|
||||
{(apiErrorMessage || (errorPage && novedades.length === 0 && !loading)) && (
|
||||
<Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage || errorPage}</Alert>
|
||||
)}
|
||||
|
||||
{loading && <Box sx={{display:'flex', justifyContent:'center', my:2}}><CircularProgress size={30} /></Box>}
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Fecha</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold', width: '70%' }}>Detalle de Novedad</TableCell>
|
||||
{puedeGestionarNovedades && <TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{novedades.length === 0 && !loading ? (
|
||||
<TableRow><TableCell colSpan={puedeGestionarNovedades ? 3 : 2} align="center">
|
||||
No hay novedades registradas { (filtroFechaDesde || filtroFechaHasta) && "con los filtros aplicados"}.
|
||||
</TableCell></TableRow>
|
||||
) : (
|
||||
novedades.map((nov) => (
|
||||
<TableRow key={nov.idNovedad} hover>
|
||||
<TableCell>{formatDate(nov.fecha)}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={nov.detalle || ''} arrow>
|
||||
<Typography variant="body2" sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '500px'
|
||||
}}>
|
||||
{nov.detalle || '-'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{puedeGestionarNovedades && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, nov)} disabled={!puedeGestionarNovedades}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeGestionarNovedades && selectedNovedadRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedNovedadRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar</MenuItem>)}
|
||||
{puedeGestionarNovedades && selectedNovedadRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedNovedadRow.idNovedad)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
{idCanilla &&
|
||||
<NovedadCanillaFormModal
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
idCanilla={idCanilla}
|
||||
nombreCanilla={canillita?.nomApe}
|
||||
initialData={editingNovedad}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionarNovedadesCanillaPage;
|
||||
@@ -37,15 +37,16 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad
|
||||
const puedeVer = isSuperAdmin || tienePermiso("OD001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("OD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("OD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("OD004");
|
||||
|
||||
const cargarOtrosDestinos = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver esta sección.");
|
||||
setError("No tiene permiso para ver los 'Otros Destinos'.");
|
||||
setLoading(false);
|
||||
setOtrosDestinos([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -131,8 +132,8 @@ const GestionarOtrosDestinosPage: React.FC = () => {
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h4" gutterBottom>Gestionar Otros Destinos</Typography>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Gestionar Otros Destinos</Typography> {/* Cambiado h4 a h5 */}
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -241,7 +241,7 @@ const GestionarPublicacionesPage: React.FC = () => {
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={filtroSoloHabilitadas === undefined ? true : filtroSoloHabilitadas} onChange={(e) => setFiltroSoloHabilitadas(e.target.checked)} size="small" />}
|
||||
label="Solo Habilitadas"
|
||||
label="Ver Habilitadas"
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Publicación</Button>)}
|
||||
|
||||
@@ -37,13 +37,19 @@ const GestionarZonasPage: React.FC = () => {
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Ajustar códigos de permiso para Zonas
|
||||
const puedeVer = isSuperAdmin || tienePermiso("ZD001"); // Permiso para ver Zonas
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("ZD002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("ZD003");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("ZD004");
|
||||
|
||||
|
||||
const cargarZonas = useCallback(async () => {
|
||||
if (!puedeVer) {
|
||||
setError("No tiene permiso para ver las zonas.");
|
||||
setLoading(false);
|
||||
setZonas([]); // Asegurar que no se muestren datos previos
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -134,6 +140,17 @@ const GestionarZonasPage: React.FC = () => {
|
||||
// Adaptar para paginación
|
||||
const displayData = zonas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
if (!loading && !puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Gestionar Zonas
|
||||
</Typography>
|
||||
{/* El error de "sin permiso" ya fue seteado en cargarZonas */}
|
||||
<Alert severity="error">{error || "No tiene permiso para acceder a esta sección."}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
@@ -150,7 +167,6 @@ const GestionarZonasPage: React.FC = () => {
|
||||
value={filtroNombre}
|
||||
onChange={(e) => setFiltroNombre(e.target.value)}
|
||||
/>
|
||||
{/* <TextField label="Filtrar por Descripción" ... /> */}
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
@@ -165,11 +181,11 @@ const GestionarZonasPage: React.FC = () => {
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{error && !loading && puedeVer && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
|
||||
{!loading && !error && (
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
||||
@@ -283,8 +283,8 @@ const ReporteComparativaConsumoBobinasPage: React.FC = () => {
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Box, Typography, Paper, CircularProgress, Alert, Button, Divider, type
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ControlDevolucionesDataResponseDto } from '../../models/dtos/Reportes/ControlDevolucionesDataResponseDto';
|
||||
import SeleccionaReporteControlDevoluciones from './SeleccionaReporteControlDevoluciones';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
// ... (estados y funciones de manejo de datos sin cambios significativos, excepto cómo se renderiza) ...
|
||||
const [reportData, setReportData] = useState<ControlDevolucionesDataResponseDto | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
@@ -17,6 +17,8 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{ fecha: string; idEmpresa: number; nombreEmpresa?: string } | null>(null);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR003");
|
||||
|
||||
const numberLocaleFormatter = (value: number | null | undefined, showSign = false): string => {
|
||||
if (value == null) return '';
|
||||
@@ -26,6 +28,11 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: { fecha: string; idEmpresa: number }) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
@@ -159,89 +166,89 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
}, [reportData]);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!reportData || !calculatedValues || !currentParams) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
if (!reportData || !calculatedValues || !currentParams) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
|
||||
const dataForExcel: any[][] = [];
|
||||
const dataForExcel: any[][] = [];
|
||||
|
||||
// --- Títulos y Cabecera ---
|
||||
dataForExcel.push(["Control de Devoluciones"]); // Título Principal
|
||||
dataForExcel.push(["Canillas / Accionistas"]); // Subtítulo
|
||||
dataForExcel.push([]); // Fila vacía para espaciado
|
||||
// --- Títulos y Cabecera ---
|
||||
dataForExcel.push(["Control de Devoluciones"]); // Título Principal
|
||||
dataForExcel.push(["Canillas / Accionistas"]); // Subtítulo
|
||||
dataForExcel.push([]); // Fila vacía para espaciado
|
||||
|
||||
dataForExcel.push([
|
||||
`Fecha Consultada: ${currentParams.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''}`,
|
||||
"", // Celda vacía para espaciado o para alinear con la segunda columna si fuera necesario
|
||||
`Cantidad Canillas: ${calculatedValues.cantidadCanillas}`
|
||||
]);
|
||||
dataForExcel.push([]);
|
||||
dataForExcel.push([
|
||||
`Fecha Consultada: ${currentParams.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}`,
|
||||
"", // Celda vacía para espaciado o para alinear con la segunda columna si fuera necesario
|
||||
`Cantidad Canillas: ${calculatedValues.cantidadCanillas}`
|
||||
]);
|
||||
dataForExcel.push([]);
|
||||
|
||||
dataForExcel.push([currentParams.nombreEmpresa || 'EL DIA']); // Nombre de la Empresa/Publicación
|
||||
dataForExcel.push([]);
|
||||
dataForExcel.push([currentParams.nombreEmpresa || 'EL DIA']); // Nombre de la Empresa/Publicación
|
||||
dataForExcel.push([]);
|
||||
|
||||
// --- Cuerpo del Reporte ---
|
||||
dataForExcel.push(["Ingresados por Remito:", calculatedValues.ingresadosPorRemito]);
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]); // Línea divisoria (estilo simple)
|
||||
// --- Cuerpo del Reporte ---
|
||||
dataForExcel.push(["Ingresados por Remito:", calculatedValues.ingresadosPorRemito]);
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]); // Línea divisoria (estilo simple)
|
||||
|
||||
dataForExcel.push(["Accionistas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosAcc]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosAcc]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalAcc]); // Fila de Total con estilo
|
||||
dataForExcel.push([]);
|
||||
dataForExcel.push(["Accionistas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosAcc]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosAcc]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalAcc]); // Fila de Total con estilo
|
||||
dataForExcel.push([]);
|
||||
|
||||
dataForExcel.push(["Canillitas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosCan]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosCan]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalCan]); // Fila de Total con estilo
|
||||
dataForExcel.push(["==================================", "==================="]); // Línea divisoria sólida
|
||||
dataForExcel.push(["Canillitas"]);
|
||||
dataForExcel.push(["Llevados", -calculatedValues.llevadosCan]);
|
||||
dataForExcel.push(["Devueltos", calculatedValues.devueltosCan]);
|
||||
dataForExcel.push(["Total", -calculatedValues.totalCan]); // Fila de Total con estilo
|
||||
dataForExcel.push(["==================================", "==================="]); // Línea divisoria sólida
|
||||
|
||||
dataForExcel.push(["Total Devolución a la Fecha", calculatedValues.totalDevolucionFecha]);
|
||||
dataForExcel.push(["Total Devolución Días Anteriores", calculatedValues.totalDevolucionOtrosDias]);
|
||||
dataForExcel.push(["Total Devolución", calculatedValues.totalDevolucion]); // Fila de Total con estilo
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]);
|
||||
dataForExcel.push(["Total Devolución a la Fecha", calculatedValues.totalDevolucionFecha]);
|
||||
dataForExcel.push(["Total Devolución Días Anteriores", calculatedValues.totalDevolucionOtrosDias]);
|
||||
dataForExcel.push(["Total Devolución", calculatedValues.totalDevolucion]); // Fila de Total con estilo
|
||||
dataForExcel.push(["----------------------------------", "-------------------"]);
|
||||
|
||||
dataForExcel.push(["Sin Cargo", calculatedValues.sinCargo]);
|
||||
dataForExcel.push(["Sobrantes", -calculatedValues.sobrantes]);
|
||||
dataForExcel.push(["Diferencia", calculatedValues.diferencia]); // Fila de Total con estilo
|
||||
dataForExcel.push(["Sin Cargo", calculatedValues.sinCargo]);
|
||||
dataForExcel.push(["Sobrantes", -calculatedValues.sobrantes]);
|
||||
dataForExcel.push(["Diferencia", calculatedValues.diferencia]); // Fila de Total con estilo
|
||||
|
||||
// --- Crear Hoja y Libro ---
|
||||
const ws = XLSX.utils.aoa_to_sheet(dataForExcel);
|
||||
// --- Crear Hoja y Libro ---
|
||||
const ws = XLSX.utils.aoa_to_sheet(dataForExcel);
|
||||
|
||||
// Ajustar anchos de columna (opcional, pero recomendado)
|
||||
// Esto es un cálculo aproximado, puedes ajustarlo
|
||||
const colWidths = [
|
||||
{ wch: 40 }, // Columna A (Etiquetas)
|
||||
{ wch: 15 }, // Columna B (Valores)
|
||||
{ wch: 25 } // Columna C (para Cantidad Canillas)
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
// Ajustar anchos de columna (opcional, pero recomendado)
|
||||
// Esto es un cálculo aproximado, puedes ajustarlo
|
||||
const colWidths = [
|
||||
{ wch: 40 }, // Columna A (Etiquetas)
|
||||
{ wch: 15 }, // Columna B (Valores)
|
||||
{ wch: 25 } // Columna C (para Cantidad Canillas)
|
||||
];
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
// Fusionar celdas para títulos (opcional, requiere más trabajo con la estructura de 'ws')
|
||||
// Ejemplo para el título principal (ocuparía A1:C1)
|
||||
if (!ws['!merges']) ws['!merges'] = [];
|
||||
ws['!merges'].push({ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }); // Fusionar A1 a C1
|
||||
ws['!merges'].push({ s: { r: 1, c: 0 }, e: { r: 1, c: 2 } }); // Fusionar A2 a C2
|
||||
ws['!merges'].push({ s: { r: 5, c: 0 }, e: { r: 5, c: 2 } }); // Fusionar celda de Nombre Empresa
|
||||
// Fusionar celdas para títulos (opcional, requiere más trabajo con la estructura de 'ws')
|
||||
// Ejemplo para el título principal (ocuparía A1:C1)
|
||||
if (!ws['!merges']) ws['!merges'] = [];
|
||||
ws['!merges'].push({ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }); // Fusionar A1 a C1
|
||||
ws['!merges'].push({ s: { r: 1, c: 0 }, e: { r: 1, c: 2 } }); // Fusionar A2 a C2
|
||||
ws['!merges'].push({ s: { r: 5, c: 0 }, e: { r: 5, c: 2 } }); // Fusionar celda de Nombre Empresa
|
||||
|
||||
|
||||
// Aplicar formato numérico (esto es más avanzado y depende de cómo quieras los números en Excel)
|
||||
// Por ahora, los números se exportarán como números si son de tipo number en dataForExcel.
|
||||
// Para formato de moneda o miles, tendrías que modificar las celdas en el objeto 'ws'
|
||||
// o asegurarte de que los valores en 'dataForExcel' ya estén como strings formateados si Excel no los interpreta bien.
|
||||
// Por simplicidad, los dejamos como números y Excel usará su formato por defecto.
|
||||
// Aplicar formato numérico (esto es más avanzado y depende de cómo quieras los números en Excel)
|
||||
// Por ahora, los números se exportarán como números si son de tipo number en dataForExcel.
|
||||
// Para formato de moneda o miles, tendrías que modificar las celdas en el objeto 'ws'
|
||||
// o asegurarte de que los valores en 'dataForExcel' ya estén como strings formateados si Excel no los interpreta bien.
|
||||
// Por simplicidad, los dejamos como números y Excel usará su formato por defecto.
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ControlDevoluciones");
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "ControlDevoluciones");
|
||||
|
||||
let fileName = "ReporteControlDevoluciones";
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fecha}`;
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
let fileName = "ReporteControlDevoluciones";
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fecha}`;
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
|
||||
}, [reportData, calculatedValues, currentParams]);
|
||||
}, [reportData, calculatedValues, currentParams]);
|
||||
|
||||
|
||||
|
||||
@@ -306,6 +313,9 @@ const ReporteControlDevolucionesPage: React.FC = () => {
|
||||
);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Button
|
||||
Box, Typography, Paper, CircularProgress, Button,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
@@ -9,6 +10,7 @@ import type { ReporteCuentasDistribuidorResponseDto } from '../../models/dtos/Re
|
||||
import type { BalanceCuentaDistDto } from '../../models/dtos/Reportes/BalanceCuentaDistDto';
|
||||
import type { BalanceCuentaDebCredDto } from '../../models/dtos/Reportes/BalanceCuentaDebCredDto';
|
||||
import type { BalanceCuentaPagosDto } from '../../models/dtos/Reportes/BalanceCuentaPagosDto';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import SeleccionaReporteCuentasDistribuidores from './SeleccionaReporteCuentasDistribuidores';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
@@ -20,6 +22,7 @@ type PagoConSaldo = BalanceCuentaPagosDto & { id: string; saldoAcumulado: number
|
||||
const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const [originalReportData, setOriginalReportData] = useState<ReporteCuentasDistribuidorResponseDto | null>(null);
|
||||
const [movimientosConSaldo, setMovimientosConSaldo] = useState<MovimientoConSaldo[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notasConSaldo, setNotasConSaldo] = useState<NotaConSaldo[]>([]);
|
||||
const [pagosConSaldo, setPagosConSaldo] = useState<PagoConSaldo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
@@ -34,6 +37,8 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
nombreDistribuidor?: string;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR001");
|
||||
|
||||
// Calcula saldos acumulados seccion por seccion
|
||||
const calcularSaldosPorSeccion = (data: ReporteCuentasDistribuidorResponseDto) => {
|
||||
@@ -227,6 +232,12 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
setApiErrorParams(null);
|
||||
setOriginalReportData(null);
|
||||
@@ -237,8 +248,8 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const distSvc = (await import('../../services/Distribucion/distribuidorService')).default;
|
||||
const empSvc = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const [distData, empData] = await Promise.all([
|
||||
distSvc.getDistribuidorById(params.idDistribuidor),
|
||||
empSvc.getEmpresaById(params.idEmpresa)
|
||||
distSvc.getDistribuidorLookupById(params.idDistribuidor),
|
||||
empSvc.getEmpresaLookupById(params.idEmpresa)
|
||||
]);
|
||||
setCurrentParams({
|
||||
...params,
|
||||
@@ -273,20 +284,39 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!originalReportData) return;
|
||||
const wb = XLSX.utils.book_new();
|
||||
if (movimientosConSaldo.length) {
|
||||
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 wb = XLSX.utils.book_new();// Se crea un nuevo libro
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -297,13 +327,16 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
const blob = await reportesService.getReporteCuentasDistribuidorPdf(currentParams);
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} catch {
|
||||
/* manejar error */
|
||||
setError('Ocurrió un error al generar el PDF.');
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
@@ -328,6 +361,11 @@ const ReporteCuentasDistribuidoresPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
{error && (
|
||||
<Paper sx={{ mb: 2, p: 2, backgroundColor: '#ffeaea' }}>
|
||||
<Typography color="error">{error}</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h5">Cuenta Corriente Distribuidor</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
|
||||
404
Frontend/src/pages/Reportes/ReporteListadoDistMensualPage.tsx
Normal file
404
Frontend/src/pages/Reportes/ReporteListadoDistMensualPage.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
// src/pages/Reportes/ReporteListadoDistMensualPage.tsx
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistCanMensualDiariosDto } from '../../models/dtos/Reportes/ListadoDistCanMensualDiariosDto';
|
||||
import type { ListadoDistCanMensualPubDto } from '../../models/dtos/Reportes/ListadoDistCanMensualPubDto';
|
||||
import SeleccionaReporteListadoDistMensual, { type TipoListadoDistMensual } from './SeleccionaReporteListadoDistMensual';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
// Interfaces para DataGrid (añadiendo 'id')
|
||||
interface GridDiariosItem extends ListadoDistCanMensualDiariosDto { id: string; }
|
||||
interface GridPubItem extends ListadoDistCanMensualPubDto { id: string; }
|
||||
|
||||
|
||||
const ReporteListadoDistMensualPage: React.FC = () => {
|
||||
const [reporteDiariosData, setReporteDiariosData] = useState<GridDiariosItem[]>([]);
|
||||
const [reportePubData, setReportePubData] = useState<GridPubItem[]>([]);
|
||||
const [currentReportVariant, setCurrentReportVariant] = useState<TipoListadoDistMensual | null>(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
esAccionista: boolean;
|
||||
tipoReporte: TipoListadoDistMensual;
|
||||
nombreTipoVendedor?: string;
|
||||
mesAnio?: string;
|
||||
} | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR009"); // Asumiendo RR009 para este reporte
|
||||
|
||||
const currencyFormatter = (value?: number | null) => value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '-';
|
||||
const numberFormatter = (value?: number | null) => value != null ? Number(value).toLocaleString('es-AR') : '-';
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
esAccionista: boolean;
|
||||
tipoReporte: TipoListadoDistMensual;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setReporteDiariosData([]);
|
||||
setReportePubData([]);
|
||||
setCurrentReportVariant(params.tipoReporte);
|
||||
|
||||
const mesAnioStr = new Date(params.fechaDesde + 'T00:00:00').toLocaleDateString('es-AR', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
||||
setCurrentParams({ ...params, nombreTipoVendedor: params.esAccionista ? "Accionistas" : "Canillitas", mesAnio: mesAnioStr });
|
||||
|
||||
try {
|
||||
if (params.tipoReporte === 'diarios') {
|
||||
const data = await reportesService.getListadoDistMensualDiarios(params);
|
||||
setReporteDiariosData(data.map((item, i) => ({ ...item, id: `diario-${item.canilla}-${i}` })));
|
||||
if (data.length === 0) setError("No se encontraron datos para la variante 'Desglose por Diarios'.");
|
||||
} else { // 'publicaciones'
|
||||
const data = await reportesService.getListadoDistMensualPorPublicacion(params);
|
||||
setReportePubData(data.map((item, i) => ({ ...item, id: `pub-${item.canilla}-${item.publicacion}-${i}` })));
|
||||
if (data.length === 0) setError("No se encontraron datos para la variante 'Desglose por Publicación'.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message : 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVerReporte]);
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
setReporteDiariosData([]);
|
||||
setReportePubData([]);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
setCurrentReportVariant(null);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (reporteDiariosData.length === 0 && reportePubData.length === 0) {
|
||||
alert("No hay datos para exportar."); return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
let fileName = "ListadoDistMensual";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreTipoVendedor?.replace(/\s+/g, '')}`;
|
||||
fileName += `_${currentParams.mesAnio?.replace(/\s+/g, '-').replace('/', '-')}`;
|
||||
}
|
||||
|
||||
if (currentReportVariant === 'diarios' && reporteDiariosData.length > 0) {
|
||||
const data = reporteDiariosData.map(({ id, ...r }) => ({
|
||||
"Canillita": r.canilla, "El Día (Cant.)": r.elDia, "El Plata (Cant.)": r.elPlata,
|
||||
"Total Vendidos": r.vendidos, "Imp. El Día": r.importeElDia, "Imp. El Plata": r.importeElPlata,
|
||||
"Importe Total": r.importeTotal
|
||||
}));
|
||||
const totalesDiarios = {
|
||||
"Canillita": "TOTALES", "El Día (Cant.)": reporteDiariosData.reduce((s, i) => s + (i.elDia ?? 0), 0),
|
||||
"El Plata (Cant.)": reporteDiariosData.reduce((s, i) => s + (i.elPlata ?? 0), 0),
|
||||
"Total Vendidos": reporteDiariosData.reduce((s, i) => s + (i.vendidos ?? 0), 0),
|
||||
"Imp. El Día": reporteDiariosData.reduce((s, i) => s + (i.importeElDia ?? 0), 0),
|
||||
"Imp. El Plata": reporteDiariosData.reduce((s, i) => s + (i.importeElPlata ?? 0), 0),
|
||||
"Importe Total": reporteDiariosData.reduce((s, i) => s + (i.importeTotal ?? 0), 0)
|
||||
};
|
||||
data.push(totalesDiarios);
|
||||
const ws = XLSX.utils.json_to_sheet(data);
|
||||
const headers = Object.keys(data[0] || {});
|
||||
ws['!cols'] = headers.map(h => ({ wch: Math.max(...data.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Desglose Diarios");
|
||||
fileName += "_Diarios.xlsx";
|
||||
|
||||
} else if (currentReportVariant === 'publicaciones' && reportePubData.length > 0) {
|
||||
// --- INICIO DE CAMBIOS PARA TOTALES EN EXCEL DE PUBLICACIONES ---
|
||||
const dataAgrupadaParaExcel: any[] = [];
|
||||
const canillitasUnicos = [...new Set(reportePubData.map(item => item.canilla))];
|
||||
|
||||
canillitasUnicos.sort().forEach(nombreCanillita => {
|
||||
const itemsDelCanillita = reportePubData.filter(item => item.canilla === nombreCanillita);
|
||||
itemsDelCanillita.forEach(item => {
|
||||
dataAgrupadaParaExcel.push({
|
||||
"Canillita": item.canilla,
|
||||
"Publicación": item.publicacion,
|
||||
"Llevados": item.totalCantSalida,
|
||||
"Devueltos": item.totalCantEntrada,
|
||||
"A Rendir": item.totalRendir
|
||||
});
|
||||
});
|
||||
// Fila de Total por Canillita
|
||||
const totalLlevadosCanillita = itemsDelCanillita.reduce((sum, item) => sum + (item.totalCantSalida ?? 0), 0);
|
||||
const totalDevueltosCanillita = itemsDelCanillita.reduce((sum, item) => sum + (item.totalCantEntrada ?? 0), 0);
|
||||
const totalRendirCanillita = itemsDelCanillita.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0);
|
||||
dataAgrupadaParaExcel.push({
|
||||
"Canillita": `Total ${nombreCanillita}`,
|
||||
"Publicación": "",
|
||||
"Llevados": totalLlevadosCanillita,
|
||||
"Devueltos": totalDevueltosCanillita,
|
||||
"A Rendir": totalRendirCanillita,
|
||||
});
|
||||
dataAgrupadaParaExcel.push({}); // Fila vacía para separar
|
||||
});
|
||||
|
||||
// Fila de Total General
|
||||
const totalGeneralLlevados = reportePubData.reduce((sum, item) => sum + (item.totalCantSalida ?? 0), 0);
|
||||
const totalGeneralDevueltos = reportePubData.reduce((sum, item) => sum + (item.totalCantEntrada ?? 0), 0);
|
||||
const totalGeneralRendir = reportePubData.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0);
|
||||
dataAgrupadaParaExcel.push({
|
||||
"Canillita": "TOTAL GENERAL",
|
||||
"Publicación": "",
|
||||
"Llevados": totalGeneralLlevados,
|
||||
"Devueltos": totalGeneralDevueltos,
|
||||
"A Rendir": totalGeneralRendir,
|
||||
});
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(dataAgrupadaParaExcel);
|
||||
const headers = ["Canillita", "Publicación", "Llevados", "Devueltos", "A Rendir"]; // Definir orden
|
||||
ws['!cols'] = headers.map(h => ({ wch: Math.max(...dataAgrupadaParaExcel.map(row => (row as any)[h]?.toString().length ?? 0), h.length) + 2 }));
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Desglose Publicaciones");
|
||||
fileName += "_Publicaciones.xlsx";
|
||||
}
|
||||
|
||||
if (wb.SheetNames.length > 0) XLSX.writeFile(wb, fileName);
|
||||
else alert("No hay datos para la variante seleccionada para exportar.");
|
||||
|
||||
}, [reporteDiariosData, reportePubData, currentReportVariant, currentParams]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
if (!currentParams) { setError("Seleccione parámetros."); return; }
|
||||
if (!puedeVerReporte) { setError("Sin permiso para PDF."); return; }
|
||||
setLoadingPdf(true); setError(null);
|
||||
try {
|
||||
let blob;
|
||||
if (currentParams.tipoReporte === 'diarios') {
|
||||
blob = await reportesService.getListadoDistMensualDiariosPdf(currentParams);
|
||||
} else {
|
||||
blob = await reportesService.getListadoDistMensualPorPublicacionPdf(currentParams);
|
||||
}
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
setLoadingPdf(false); // Asegurar que se detenga el loader
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
if (!w) alert("Permita popups para ver el PDF.");
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el PDF.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams, puedeVerReporte]);
|
||||
|
||||
const FooterSimple = () => (<GridFooterContainer><GridFooter /></GridFooterContainer>);
|
||||
|
||||
const totalGeneralDiarios = useMemo(() => {
|
||||
if (reporteDiariosData.length === 0) return null;
|
||||
return {
|
||||
elDia: reporteDiariosData.reduce((s, i) => s + (i.elDia ?? 0), 0),
|
||||
elPlata: reporteDiariosData.reduce((s, i) => s + (i.elPlata ?? 0), 0),
|
||||
vendidos: reporteDiariosData.reduce((s, i) => s + (i.vendidos ?? 0), 0),
|
||||
importeElDia: reporteDiariosData.reduce((s, i) => s + (i.importeElDia ?? 0), 0),
|
||||
importeElPlata: reporteDiariosData.reduce((s, i) => s + (i.importeElPlata ?? 0), 0),
|
||||
importeTotal: reporteDiariosData.reduce((s, i) => s + (i.importeTotal ?? 0), 0)
|
||||
};
|
||||
}, [reporteDiariosData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FooterDiarios = () => {
|
||||
if (!totalGeneralDiarios) return <GridFooterContainer><GridFooter sx={{ borderTop: 'none' }} /></GridFooterContainer>;
|
||||
return (
|
||||
<GridFooterContainer sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between', // Separa la paginación (izquierda) de los totales (derecha)
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
minHeight: '52px', // Altura estándar para el footer
|
||||
// No es necesario p: aquí si los hijos lo manejan o el GridFooterContainer lo aplica por defecto
|
||||
}}>
|
||||
{/* Box para la paginación estándar */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0, // Evita que este box se encoja si los totales son anchos
|
||||
overflow: 'hidden', // Para asegurar que no desborde si el contenido interno es muy ancho
|
||||
px: 1, // Padding horizontal para el contenedor de la paginación
|
||||
// Considera un flexGrow o un width/maxWidth si necesitas más control sobre el espacio de la paginación
|
||||
// Ejemplo: flexGrow: 1, maxWidth: 'calc(100% - 250px)' (para dejar espacio a los totales)
|
||||
}}>
|
||||
<GridFooter
|
||||
sx={{
|
||||
borderTop: 'none', // Quitar el borde superior del GridFooter interno
|
||||
width: 'auto', // Permite que el GridFooter se ajuste a su contenido (paginador)
|
||||
'& .MuiToolbar-root': { // Ajustar padding del toolbar de paginación
|
||||
paddingLeft: 0, // O un valor pequeño si es necesario
|
||||
paddingRight: 0,
|
||||
},
|
||||
// Mantenemos oculto el contador de filas seleccionadas si no lo queremos
|
||||
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* Box para los totales personalizados */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap', // Evita que los totales hagan salto de línea
|
||||
overflowX: 'auto', // Scroll DENTRO de este Box si los totales son muy anchos
|
||||
px: 2, // Padding horizontal para el contenedor de los totales (ajusta pr:2 de tu ejemplo)
|
||||
flexShrink: 1, // Permitir que este contenedor se encoja si la paginación necesita más espacio
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[0].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>TOTALES:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[1].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{numberFormatter(totalGeneralDiarios.elDia)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[2].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{numberFormatter(totalGeneralDiarios.elPlata)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[3].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{numberFormatter(totalGeneralDiarios.vendidos)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[4].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{currencyFormatter(totalGeneralDiarios.importeElDia)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[5].width, textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{currencyFormatter(totalGeneralDiarios.importeElPlata)}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsDiarios[6].width, textAlign: 'right', fontWeight: 'bold' }}>{currencyFormatter(totalGeneralDiarios.importeTotal)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const columnsDiarios: GridColDef<GridDiariosItem>[] = [
|
||||
{ field: 'canilla', headerName: 'Nombre', width: 250, flex: 1.5 },
|
||||
{ field: 'elDia', headerName: 'El Día (Cant)', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'elPlata', headerName: 'El Plata (Cant)', type: 'number', width: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'vendidos', headerName: 'Total Vendidos', type: 'number', width: 130, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'importeElDia', headerName: 'Imp. El Día', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
{ field: 'importeElPlata', headerName: 'Imp. El Plata', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
{ field: 'importeTotal', headerName: 'Importe Total', type: 'number', width: 160, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
];
|
||||
const columnsPublicaciones: GridColDef<GridPubItem>[] = [
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 250, flex: 1.2 },
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
|
||||
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(value as number) },
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(value as number) },
|
||||
];
|
||||
|
||||
const rowsDiarios = useMemo(() => reporteDiariosData, [reporteDiariosData]);
|
||||
const rowsPubs = useMemo(() => reportePubData, [reportePubData]);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteListadoDistMensual
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!loading && !puedeVerReporte && !showParamSelector) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Alert severity="error" sx={{ m: 2 }}>No tiene permiso para ver este reporte.</Alert>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Volver
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const renderReportContent = () => {
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>;
|
||||
if (error && !loading) return <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>;
|
||||
|
||||
if (currentReportVariant === 'diarios') {
|
||||
if (reporteDiariosData.length === 0) return <Typography sx={{ mt: 2, fontStyle: 'italic' }}>No hay datos para la variante "Desglose por Diarios".</Typography>;
|
||||
return (
|
||||
<Paper sx={{ height: 'calc(100vh - 320px)', width: '100%', mt: 2 }}>
|
||||
<DataGrid
|
||||
rows={rowsDiarios}
|
||||
columns={columnsDiarios}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterDiarios }}
|
||||
density="compact"
|
||||
hideFooterSelectedRowCount
|
||||
disableRowSelectionOnClick
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 100 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
if (currentReportVariant === 'publicaciones') {
|
||||
if (reportePubData.length === 0) return <Typography sx={{ mt: 2, fontStyle: 'italic' }}>No hay datos para la variante "Desglose por Publicación".</Typography>;
|
||||
return (
|
||||
<Paper sx={{ height: 'calc(100vh - 320px)', width: '100%', mt: 2 }}>
|
||||
<DataGrid
|
||||
rows={rowsPubs}
|
||||
columns={columnsPublicaciones}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterSimple }} // Para esta tabla, un footer simple sin totales complejos por ahora
|
||||
density="compact"
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 100 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Listado Distribución Mensual ({currentParams?.nombreTipoVendedor})</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || (reporteDiariosData.length === 0 && reportePubData.length === 0) || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={(reporteDiariosData.length === 0 && reportePubData.length === 0) || !!error} size="small">
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Nuevos Parámetros
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Mes: {currentParams?.mesAnio || '-'}
|
||||
</Typography>
|
||||
{renderReportContent()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteListadoDistMensualPage;
|
||||
@@ -8,6 +8,7 @@ import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ListadoDistribucionDistribuidoresResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionDistribuidoresResponseDto';
|
||||
import SeleccionaReporteListadoDistribucion from './SeleccionaReporteListadoDistribucion';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -26,6 +27,8 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
nombrePublicacion?: string;
|
||||
nombreDistribuidor?: string;
|
||||
} | null>(null);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR002");
|
||||
|
||||
// --- ESTADO PARA TOTALES CALCULADOS (PARA EL FOOTER DEL DETALLE) ---
|
||||
const [totalesDetalle, setTotalesDetalle] = useState({
|
||||
@@ -51,12 +54,17 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setReportData(null);
|
||||
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;
|
||||
@@ -99,11 +107,11 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
|
||||
|
||||
setTotalesDetalle({
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: diasConVenta > 0 ? totalVentaNetaDetalle / diasConVenta : 0,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: diasConVenta > 0 ? totalVentaNetaDetalle / diasConVenta : 0,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
});
|
||||
|
||||
|
||||
@@ -190,7 +198,7 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
"Prom. Ventas": rest.promedio_Ventas,
|
||||
"% Devolución": (rest as any).porcentajeDevolucion, // Ya calculado
|
||||
}));
|
||||
// Fila de totales para promedios
|
||||
// Fila de totales para promedios
|
||||
promediosToExport.push({
|
||||
"Día Semana": "General",
|
||||
"Cant. Días": totalesPromedios.cantDias,
|
||||
@@ -222,7 +230,7 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getListadoDistribucionDistribuidoresPdf(currentParams);
|
||||
if (blob.type === "application/json") {
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
@@ -278,33 +286,33 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
flexShrink: 0, // Evita que se encoja demasiado si los totales son muy anchos
|
||||
}}>
|
||||
<GridFooter sx={{
|
||||
borderTop: 'none',
|
||||
// '& .MuiTablePagination-toolbar': { // Para los elementos dentro del paginador
|
||||
// flexWrap: 'wrap', // Permitir que los elementos internos del paginador se envuelvan
|
||||
// justifyContent: 'flex-start',
|
||||
// },
|
||||
// '& .MuiTablePagination-spacer': { // El espaciador puede ser un problema
|
||||
// display: 'none', // Prueba quitándolo
|
||||
// }
|
||||
borderTop: 'none',
|
||||
// '& .MuiTablePagination-toolbar': { // Para los elementos dentro del paginador
|
||||
// flexWrap: 'wrap', // Permitir que los elementos internos del paginador se envuelvan
|
||||
// justifyContent: 'flex-start',
|
||||
// },
|
||||
// '& .MuiTablePagination-spacer': { // El espaciador puede ser un problema
|
||||
// display: 'none', // Prueba quitándolo
|
||||
// }
|
||||
}} />
|
||||
</Box>
|
||||
{/* Contenedor para tus totales, alineado a la derecha */}
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto', // Empuja a la derecha
|
||||
flexShrink: 1, // Permite que este se encoja si es necesario, pero no demasiado
|
||||
overflowX: 'auto', // Si los totales son muchos, que tengan su propio scroll
|
||||
whiteSpace: 'nowrap', // Evitar que los textos de totales se partan
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto', // Empuja a la derecha
|
||||
flexShrink: 1, // Permite que este se encoja si es necesario, pero no demasiado
|
||||
overflowX: 'auto', // Si los totales son muchos, que tengan su propio scroll
|
||||
whiteSpace: 'nowrap', // Evitar que los textos de totales se partan
|
||||
}}>
|
||||
{/* Mantén esta estructura, pero quizás necesitas jugar con los minWidth/flex de los Typography */}
|
||||
<Typography variant="subtitle2" sx={{ flexBasis: columnsPromedios[0].width || 'auto', minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>Generales:</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', { minimumFractionDigits: 3, maximumFractionDigits: 3 })}</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', { minimumFractionDigits: 3, maximumFractionDigits: 3 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
@@ -323,20 +331,20 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 1,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 1,
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ flexBasis: columnsPromedios[0].width || 'auto', minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>Generales:</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={{ flex: columnsPromedios[2].flex, minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[3].flex, minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[4].flex, minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</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={{ flex: columnsPromedios[2].flex, minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[3].flex, minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[4].flex, minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[5].flex, minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
@@ -344,6 +352,9 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
|
||||
362
Frontend/src/pages/Reportes/ReporteNovedadesCanillasPage.tsx
Normal file
362
Frontend/src/pages/Reportes/ReporteNovedadesCanillasPage.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button, Tooltip
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { NovedadesCanillasReporteDto } from '../../models/dtos/Reportes/NovedadesCanillasReporteDto';
|
||||
import type { CanillaGananciaReporteDto } from '../../models/dtos/Reportes/CanillaGananciaReporteDto';
|
||||
import SeleccionaReporteNovedadesCanillas from './SeleccionaReporteNovedadesCanillas';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
interface NovedadesCanillasReporteGridItem extends NovedadesCanillasReporteDto {
|
||||
id: string;
|
||||
}
|
||||
interface CanillaGananciaGridItem extends CanillaGananciaReporteDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteNovedadesCanillasPage: React.FC = () => {
|
||||
const [novedadesData, setNovedadesData] = useState<NovedadesCanillasReporteGridItem[]>([]);
|
||||
const [gananciasData, setGananciasData] = useState<CanillaGananciaGridItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("RR004");
|
||||
|
||||
const currencyFormatter = (value: number | null | undefined) => // Helper para formato moneda
|
||||
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
|
||||
|
||||
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setNovedadesData([]);
|
||||
setGananciasData([]); // Limpiar datos de ganancias
|
||||
|
||||
let empresaNombre = `Empresa ${params.idEmpresa}`;
|
||||
try {
|
||||
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empresaService.getEmpresaLookupById(params.idEmpresa);
|
||||
if (empData) empresaNombre = empData.nombre;
|
||||
} catch (e) { console.warn("No se pudo obtener nombre de empresa para el reporte", e); }
|
||||
|
||||
setCurrentParams({ ...params, nombreEmpresa: empresaNombre });
|
||||
|
||||
try {
|
||||
// Llamadas concurrentes para ambos conjuntos de datos
|
||||
const [novedadesResult, gananciasResult] = await Promise.all([
|
||||
reportesService.getNovedadesCanillasReporte(params),
|
||||
reportesService.getCanillasGananciasReporte(params) // << LLAMAR AL NUEVO SERVICIO
|
||||
]);
|
||||
|
||||
const novedadesConIds = novedadesResult.map((item, index) => ({
|
||||
...item,
|
||||
id: `nov-${item.nomApe || 'sinnom'}-${item.fecha || 'sinfec'}-${index}`
|
||||
}));
|
||||
setNovedadesData(novedadesConIds);
|
||||
|
||||
const gananciasConIds = gananciasResult.map((item, index) => ({
|
||||
...item,
|
||||
id: `gan-${item.canilla || 'sincan'}-${index}`
|
||||
}));
|
||||
setGananciasData(gananciasConIds);
|
||||
|
||||
if (novedadesConIds.length === 0 && gananciasConIds.length === 0) {
|
||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el reporte.';
|
||||
setApiErrorParams(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVerReporte]);
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
setNovedadesData([]);
|
||||
setGananciasData([]); // Limpiar también
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (novedadesData.length === 0 && gananciasData.length === 0) { // Chequear ambos
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Hoja de Ganancias
|
||||
if (gananciasData.length > 0) {
|
||||
const gananciasToExport = gananciasData.map(({ id, ...rest }) => ({
|
||||
"Canilla": rest.canilla,
|
||||
"Legajo": rest.legajo ?? '-',
|
||||
"Faltas": rest.faltas ?? 0,
|
||||
"Francos": rest.francos ?? 0,
|
||||
"Comisiones": rest.totalRendir ?? 0,
|
||||
}));
|
||||
const totalComisiones = gananciasData.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0);
|
||||
gananciasToExport.push({
|
||||
"Canilla": "Total", "Legajo": "", "Faltas": 0, "Francos": 0,
|
||||
"Comisiones": totalComisiones
|
||||
});
|
||||
const wsGanancias = XLSX.utils.json_to_sheet(gananciasToExport);
|
||||
const headersGan = Object.keys(gananciasToExport[0] || {});
|
||||
wsGanancias['!cols'] = headersGan.map(h => {
|
||||
const maxLen = Math.max(...gananciasToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
XLSX.utils.book_append_sheet(wb, wsGanancias, "ResumenCanillas");
|
||||
}
|
||||
|
||||
// Hoja de Novedades
|
||||
if (novedadesData.length > 0) {
|
||||
const novedadesToExport = novedadesData.map(({ id, ...rest }) => ({
|
||||
"Canillita": rest.nomApe,
|
||||
"Fecha": rest.fecha ? new Date(rest.fecha).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-',
|
||||
"Detalle": rest.detalle,
|
||||
}));
|
||||
const wsNovedades = XLSX.utils.json_to_sheet(novedadesToExport);
|
||||
const headersNov = Object.keys(novedadesToExport[0] || {});
|
||||
wsNovedades['!cols'] = headersNov.map(h => {
|
||||
const maxLen = Math.max(...novedadesToExport.map(row => (row as any)[h]?.toString().length ?? 0), h.length);
|
||||
if (h === "Detalle") return { wch: Math.max(maxLen + 2, 50) };
|
||||
return { wch: maxLen + 2 };
|
||||
});
|
||||
XLSX.utils.book_append_sheet(wb, wsNovedades, "DetalleNovedades");
|
||||
}
|
||||
|
||||
let fileName = "ReporteNovedadesCanillas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') || `Emp${currentParams.idEmpresa}`}`;
|
||||
fileName += `_${currentParams.fechaDesde}_a_${currentParams.fechaHasta}`;
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [novedadesData, gananciasData, currentParams]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
// ... (sin cambios, ya que el PDF del backend ya debería estar manejando ambos DataSets)
|
||||
if (!currentParams) {
|
||||
setError("Primero debe generar el reporte en pantalla.");
|
||||
return;
|
||||
}
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este PDF.");
|
||||
return;
|
||||
}
|
||||
setLoadingPdf(true);
|
||||
setError(null);
|
||||
try {
|
||||
const blob = await reportesService.getNovedadesCanillasReportePdf(currentParams);
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
const msg = JSON.parse(text).message ?? "Error inesperado al generar PDF.";
|
||||
setError(msg);
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, '_blank');
|
||||
if (!w) alert("Permita popups para ver el PDF.");
|
||||
}
|
||||
} catch (err: any){
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Ocurrió un error al generar el PDF.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoadingPdf(false);
|
||||
}
|
||||
}, [currentParams, puedeVerReporte]);
|
||||
|
||||
// Columnas para la tabla de Resumen/Ganancias
|
||||
const columnsGanancias: GridColDef<CanillaGananciaGridItem>[] = [
|
||||
{ field: 'canilla', headerName: 'Canilla', width: 250, flex: 1.5 },
|
||||
{ field: 'legajo', headerName: 'Legajo', width: 100, type: 'number' },
|
||||
{ field: 'faltas', headerName: 'Faltas', width: 100, type: 'number', align: 'right', headerAlign: 'right' },
|
||||
{ field: 'francos', headerName: 'Francos', width: 100, type: 'number', align: 'right', headerAlign: 'right' },
|
||||
{ field: 'totalRendir', headerName: 'Comisiones', width: 150, type: 'number', align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
// Columnas para la tabla de Detalles de Novedades (ya existentes)
|
||||
const columnsNovedades: GridColDef<NovedadesCanillasReporteGridItem>[] = [
|
||||
{ field: 'nomApe', headerName: 'Canillita', width: 250, flex: 1.5 },
|
||||
{
|
||||
field: 'fecha',
|
||||
headerName: 'Fecha',
|
||||
width: 120,
|
||||
type: 'date',
|
||||
valueGetter: (value) => value ? new Date(value as string) : null,
|
||||
valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', {timeZone: 'UTC'}) : '-',
|
||||
},
|
||||
{ field: 'detalle', headerName: 'Detalle Novedad', flex: 2, minWidth: 350,
|
||||
renderCell: (params) => (
|
||||
<Tooltip title={params.value || ''} arrow placement="top">
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', width: '100%' }}>
|
||||
{params.value || '-'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const rowsGanancias = useMemo(() => gananciasData, [gananciasData]);
|
||||
const rowsNovedades = useMemo(() => novedadesData, [novedadesData]);
|
||||
|
||||
const totalComisionesGanancias = useMemo(() =>
|
||||
gananciasData.reduce((sum, item) => sum + (item.totalRendir ?? 0), 0),
|
||||
[gananciasData]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FooterGanancias = () => (
|
||||
<GridFooterContainer sx={{ justifyContent: 'flex-end', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}>
|
||||
<GridFooter />
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold' }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 2 }}>Total Comisiones:</Typography>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>{currencyFormatter(totalComisionesGanancias)}</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
);
|
||||
const FooterNovedades = () => ( <GridFooterContainer sx={{ justifyContent: 'flex-end', borderTop: (theme) => `1px solid ${theme.palette.divider}` }}><GridFooter /></GridFooterContainer>);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteNovedadesCanillas
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!loading && !puedeVerReporte && !showParamSelector) {
|
||||
// ... (renderizado de "sin permiso" sin cambios)
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Alert severity="error" sx={{ m: 2 }}>No tiene permiso para ver este reporte.</Alert>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Volver
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Listado de Novedades Canillitas</Typography> {/* Título más genérico */}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || (novedadesData.length === 0 && gananciasData.length === 0) || !!error} size="small">
|
||||
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={(novedadesData.length === 0 && gananciasData.length === 0) || !!error} size="small">
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
<Button onClick={handleVolverAParametros} variant="outlined" color="secondary" size="small">
|
||||
Nuevos Parámetros
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Empresa: {currentParams?.nombreEmpresa || '-'} |
|
||||
Período: {currentParams?.fechaDesde ? new Date(currentParams.fechaDesde + 'T00:00:00').toLocaleDateString('es-AR') : ''} al {currentParams?.fechaHasta ? new Date(currentParams.fechaHasta + 'T00:00:00').toLocaleDateString('es-AR') : ''}
|
||||
</Typography>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{(error && !loading) && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{/* Sección de Ganancias/Resumen */}
|
||||
{!loading && !error && currentParams && (
|
||||
<>
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>Resumen de Actividad</Typography>
|
||||
{gananciasData.length > 0 ? (
|
||||
<Paper sx={{ height: 250, width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={rowsGanancias}
|
||||
columns={columnsGanancias}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterGanancias }}
|
||||
density="compact"
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
rowHeight={48}
|
||||
sx={{
|
||||
'& .MuiDataGrid-cell': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiDataGrid-columnHeaderTitleContainer': { overflow: 'hidden' },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{mt:1, mb:3, fontStyle:'italic'}}>No hay datos de resumen de actividad para mostrar.</Typography>
|
||||
)}
|
||||
|
||||
{/* Sección de Detalle de Novedades */}
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>Otras Novedades (Detalle)</Typography>
|
||||
{novedadesData.length > 0 ? (
|
||||
<Paper sx={{ height: 250, width: '100%', mb: 3 }}> {/* Ajustar altura si es necesario */}
|
||||
<DataGrid
|
||||
rows={rowsNovedades}
|
||||
columns={columnsNovedades}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
slots={{ footer: FooterNovedades }}
|
||||
density="compact"
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
disableRowSelectionOnClick
|
||||
rowHeight={48}
|
||||
sx={{
|
||||
'& .MuiDataGrid-cell': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiDataGrid-columnHeaderTitleContainer': { overflow: 'hidden' },
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography sx={{mt:1, fontStyle:'italic'}}>No hay detalles de otras novedades para mostrar.</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && !error && novedadesData.length === 0 && gananciasData.length === 0 && currentParams && (
|
||||
<Typography sx={{mt:2, fontStyle:'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReporteNovedadesCanillasPage;
|
||||
@@ -21,12 +21,15 @@ const allReportModules: { category: string; label: string; path: string }[] = [
|
||||
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' },
|
||||
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
|
||||
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' },
|
||||
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' },
|
||||
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' },
|
||||
];
|
||||
|
||||
const predefinedCategoryOrder = [
|
||||
'Balance de Cuentas',
|
||||
'Listados Distribución',
|
||||
'Ctrl. Devoluciones',
|
||||
'Novedades de Canillitas',
|
||||
'Existencia Papel',
|
||||
'Movimientos Bobinas',
|
||||
'Consumos Bobinas',
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
|
||||
interface SeleccionaReporteControlDevolucionesProps {
|
||||
onGenerarReporte: (params: {
|
||||
@@ -24,7 +24,7 @@ const SeleccionaReporteControlDevoluciones: React.FC<SeleccionaReporteControlDev
|
||||
}) => {
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingEmpresas, setLoadingEmpresas] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
@@ -32,7 +32,7 @@ const SeleccionaReporteControlDevoluciones: React.FC<SeleccionaReporteControlDev
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingEmpresas(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas(); // Solo habilitadas
|
||||
const data = await empresaService.getEmpresasDropdown(); // Solo habilitadas
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas:", error);
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
interface SeleccionaReporteCuentasDistribuidoresProps {
|
||||
@@ -30,8 +30,8 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -40,8 +40,8 @@ const SeleccionaReporteCuentasDistribuidores: React.FC<SeleccionaReporteCuentasD
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(), // Asume que este servicio existe
|
||||
empresaService.getAllEmpresas() // Asume que este servicio existe
|
||||
distribuidorService.getAllDistribuidoresDropdown(), // Asume que este servicio existe
|
||||
empresaService.getEmpresasDropdown() // Asume que este servicio existe
|
||||
]);
|
||||
setDistribuidores(distData.map(d => d)); // El servicio devuelve tupla
|
||||
setEmpresas(empData);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl,
|
||||
ToggleButtonGroup, ToggleButton, RadioGroup, FormControlLabel, Radio
|
||||
} from '@mui/material';
|
||||
|
||||
export type TipoListadoDistMensual = 'diarios' | 'publicaciones';
|
||||
|
||||
interface SeleccionaReporteListadoDistMensualProps {
|
||||
onGenerarReporte: (params: {
|
||||
fechaDesde: string; // yyyy-MM-dd (primer día del mes)
|
||||
fechaHasta: string; // yyyy-MM-dd (último día del mes)
|
||||
esAccionista: boolean;
|
||||
tipoReporte: TipoListadoDistMensual;
|
||||
}) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
isLoading?: boolean;
|
||||
apiErrorMessage?: string | null;
|
||||
}
|
||||
|
||||
const SeleccionaReporteListadoDistMensual: React.FC<SeleccionaReporteListadoDistMensualProps> = ({
|
||||
onGenerarReporte,
|
||||
isLoading,
|
||||
apiErrorMessage
|
||||
}) => {
|
||||
const [mesAnio, setMesAnio] = useState<string>(new Date().toISOString().substring(0, 7)); // Formato "YYYY-MM"
|
||||
const [esAccionista, setEsAccionista] = useState<boolean>(false); // Default a Canillitas
|
||||
const [tipoReporte, setTipoReporte] = useState<TipoListadoDistMensual>('publicaciones'); // Default a Por Publicación
|
||||
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!mesAnio) errors.mesAnio = 'Debe seleccionar un Mes/Año.';
|
||||
// esAccionista y tipoReporte siempre tendrán un valor debido a los defaults y ToggleButton/RadioGroup
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleGenerar = () => {
|
||||
if (!validate()) return;
|
||||
|
||||
const [year, month] = mesAnio.split('-').map(Number);
|
||||
const fechaDesde = new Date(year, month - 1, 1).toISOString().split('T')[0];
|
||||
const fechaHasta = new Date(year, month, 0).toISOString().split('T')[0]; // Último día del mes
|
||||
|
||||
onGenerarReporte({
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
esAccionista,
|
||||
tipoReporte
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Parámetros: Listado Distribución Mensual
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Mes y Año"
|
||||
type="month"
|
||||
value={mesAnio}
|
||||
onChange={(e) => { setMesAnio(e.target.value); setLocalErrors(p => ({ ...p, mesAnio: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.mesAnio}
|
||||
helperText={localErrors.mesAnio}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading}>
|
||||
<Typography component="legend" variant="subtitle2" sx={{mb:0.5, color: 'rgba(0, 0, 0, 0.6)'}}>Tipo de Vendedor</Typography>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={esAccionista ? 'accionistas' : 'canillitas'}
|
||||
exclusive
|
||||
onChange={(_, newValue) => {
|
||||
if (newValue !== null) setEsAccionista(newValue === 'accionistas');
|
||||
}}
|
||||
aria-label="Tipo de Vendedor"
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="canillitas">Canillitas</ToggleButton>
|
||||
<ToggleButton value="accionistas">Accionistas</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading}>
|
||||
<Typography component="legend" variant="subtitle2" sx={{mb:0.5, color: 'rgba(0, 0, 0, 0.6)'}}>Variante del Reporte</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-label="Variante del Reporte"
|
||||
name="tipoReporte"
|
||||
value={tipoReporte}
|
||||
onChange={(e) => setTipoReporte(e.target.value as TipoListadoDistMensual)}
|
||||
>
|
||||
<FormControlLabel value="publicaciones" control={<Radio size="small" />} label="Por Publicación" />
|
||||
<FormControlLabel value="diarios" control={<Radio size="small" />} label="Por Diarios (El Día/El Plata)" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={handleGenerar} variant="contained" disabled={isLoading}>
|
||||
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeleccionaReporteListadoDistMensual;
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
|
||||
interface SeleccionaReporteListadoDistribucionProps {
|
||||
@@ -30,8 +30,8 @@ const SeleccionaReporteListadoDistribucion: React.FC<SeleccionaReporteListadoDis
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -40,8 +40,8 @@ const SeleccionaReporteListadoDistribucion: React.FC<SeleccionaReporteListadoDis
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [distData, pubData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(),
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true) // Solo habilitadas
|
||||
distribuidorService.getAllDistribuidoresDropdown(),
|
||||
publicacionService.getPublicacionesForDropdown(true) // Solo habilitadas
|
||||
]);
|
||||
setDistribuidores(distData.map(d => d));
|
||||
setPublicaciones(pubData.map(p => p));
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
interface SeleccionaReporteNovedadesCanillasProps {
|
||||
onGenerarReporte: (params: {
|
||||
idEmpresa: number;
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
}) => Promise<void>;
|
||||
onCancel?: () => void; // Opcional si se usa dentro de ReportesIndexPage
|
||||
isLoading?: boolean;
|
||||
apiErrorMessage?: string | null;
|
||||
}
|
||||
|
||||
const SeleccionaReporteNovedadesCanillas: React.FC<SeleccionaReporteNovedadesCanillasProps> = ({
|
||||
onGenerarReporte,
|
||||
// onCancel,
|
||||
isLoading,
|
||||
apiErrorMessage
|
||||
}) => {
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await empresaService.getEmpresasDropdown();
|
||||
setEmpresas(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas:", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar empresas.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
fetchEmpresas();
|
||||
}, []);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.';
|
||||
if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.';
|
||||
if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.';
|
||||
if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) {
|
||||
errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleGenerar = () => {
|
||||
if (!validate()) return;
|
||||
onGenerarReporte({
|
||||
idEmpresa: Number(idEmpresa),
|
||||
fechaDesde,
|
||||
fechaHasta
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Parámetros: Reporte Novedades de Canillitas
|
||||
</Typography>
|
||||
<FormControl fullWidth margin="normal" error={!!localErrors.idEmpresa} disabled={isLoading || loadingDropdowns}>
|
||||
<InputLabel id="empresa-novedades-select-label" required>Empresa</InputLabel>
|
||||
<Select
|
||||
labelId="empresa-novedades-select-label"
|
||||
label="Empresa"
|
||||
value={idEmpresa}
|
||||
onChange={(e) => { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
|
||||
{empresas.map((e) => (
|
||||
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
<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"
|
||||
value={fechaHasta}
|
||||
onChange={(e) => { setFechaHasta(e.target.value); setLocalErrors(p => ({ ...p, fechaHasta: null })); }}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
error={!!localErrors.fechaHasta}
|
||||
helperText={localErrors.fechaHasta}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
{/* {onCancel && <Button onClick={onCancel} color="secondary" disabled={isLoading}>Cancelar</Button>} */}
|
||||
<Button onClick={handleGenerar} variant="contained" disabled={isLoading || loadingDropdowns}>
|
||||
{isLoading ? <CircularProgress size={24} /> : 'Generar Reporte'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeleccionaReporteNovedadesCanillas;
|
||||
@@ -1,27 +1,79 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, Paper, CircularProgress, Alert
|
||||
Box, Typography, Button, Paper, CircularProgress, Alert
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import perfilService from '../../services/Usuarios/perfilService';
|
||||
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||
import { usePermissions } from '../../hooks/usePermissions'; // Para verificar si el usuario actual puede estar aquí
|
||||
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; // Renombrar para evitar conflicto
|
||||
import axios from 'axios';
|
||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; // Importar el componente
|
||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist';
|
||||
|
||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS002") return "Contables";
|
||||
if (codAcc === "SS003") return "Impresión";
|
||||
if (codAcc === "SS004") return "Reportes";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
if (codAcc === "SS006") return "Usuarios";
|
||||
return null;
|
||||
};
|
||||
|
||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
const moduloLower = permisoModulo.toLowerCase();
|
||||
if (moduloLower.includes("distribuidores") ||
|
||||
moduloLower.includes("canillas") ||
|
||||
moduloLower.includes("publicaciones distribución") ||
|
||||
moduloLower.includes("zonas distribuidores") ||
|
||||
moduloLower.includes("movimientos distribuidores") ||
|
||||
moduloLower.includes("empresas") ||
|
||||
moduloLower.includes("otros destinos") ||
|
||||
moduloLower.includes("ctrl. devoluciones") ||
|
||||
moduloLower.includes("movimientos canillas") ||
|
||||
moduloLower.includes("salidas otros destinos")) {
|
||||
return "Distribución";
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
return "Contables";
|
||||
}
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
moduloLower.includes("impresión bobinas") ||
|
||||
moduloLower.includes("impresión plantas") ||
|
||||
moduloLower.includes("tipos bobinas")) {
|
||||
return "Impresión";
|
||||
}
|
||||
if (moduloLower.includes("radios")) {
|
||||
return "Radios";
|
||||
}
|
||||
if (moduloLower.includes("usuarios") ||
|
||||
moduloLower.includes("perfiles")) {
|
||||
return "Usuarios";
|
||||
}
|
||||
if (moduloLower.includes("reportes")) {
|
||||
return "Reportes";
|
||||
}
|
||||
if (moduloLower.includes("permisos")) {
|
||||
return "Permisos (Definición)";
|
||||
}
|
||||
return permisoModulo;
|
||||
};
|
||||
|
||||
const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
const { idPerfil } = useParams<{ idPerfil: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); // Renombrado
|
||||
|
||||
const puedeAsignar = isSuperAdmin || tienePermiso("PU004");
|
||||
const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004");
|
||||
|
||||
const [perfil, setPerfil] = useState<PerfilDto | null>(null);
|
||||
const [permisosDisponibles, setPermisosDisponibles] = useState<PermisoAsignadoDto[]>([]);
|
||||
// Usamos un Set para los IDs de los permisos seleccionados para eficiencia
|
||||
const [permisosSeleccionados, setPermisosSeleccionados] = useState<Set<number>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -32,29 +84,28 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
|
||||
const cargarDatos = useCallback(async () => {
|
||||
if (!puedeAsignar) {
|
||||
setError("Acceso denegado. No tiene permiso para asignar permisos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("Acceso denegado. No tiene permiso para asignar permisos.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (isNaN(idPerfilNum)) {
|
||||
setError("ID de Perfil inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
setError("ID de Perfil inválido.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
const [perfilData, permisosData] = await Promise.all([
|
||||
perfilService.getPerfilById(idPerfilNum),
|
||||
perfilService.getPermisosPorPerfil(idPerfilNum)
|
||||
perfilService.getPermisosPorPerfil(idPerfilNum) // Esto devuelve todos los permisos con su estado 'asignado'
|
||||
]);
|
||||
setPerfil(perfilData);
|
||||
setPermisosDisponibles(permisosData);
|
||||
// Inicializar los permisos seleccionados basados en los que vienen 'asignado: true'
|
||||
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.');
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
setError(`Perfil con ID ${idPerfilNum} no encontrado.`);
|
||||
}
|
||||
} finally {
|
||||
@@ -66,22 +117,83 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
cargarDatos();
|
||||
}, [cargarDatos]);
|
||||
|
||||
const handlePermisoChange = (permisoId: number, asignado: boolean) => {
|
||||
setPermisosSeleccionados(prev => {
|
||||
const next = new Set(prev);
|
||||
if (asignado) {
|
||||
next.add(permisoId);
|
||||
} else {
|
||||
next.delete(permisoId);
|
||||
}
|
||||
return next;
|
||||
const handlePermisoChange = useCallback((
|
||||
permisoId: number,
|
||||
asignadoViaCheckboxHijo: boolean, // Este valor es el 'e.target.checked' si el clic fue en un hijo
|
||||
esPermisoSeccionClick = false,
|
||||
moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo
|
||||
) => {
|
||||
setPermisosSeleccionados(prevSelected => {
|
||||
const newSelected = new Set(prevSelected);
|
||||
const permisoActual = permisosDisponibles.find(p => p.id === permisoId);
|
||||
if (!permisoActual) return prevSelected;
|
||||
|
||||
const permisosDelModuloHijo = moduloConceptualAsociado
|
||||
? permisosDisponibles.filter(p => {
|
||||
const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper
|
||||
return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX);
|
||||
})
|
||||
: [];
|
||||
|
||||
if (esPermisoSeccionClick && moduloConceptualAsociado) {
|
||||
const idPermisoSeccion = permisoActual.id;
|
||||
const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion);
|
||||
const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id));
|
||||
const ningunHijoEstabaSeleccionado = permisosDelModuloHijo.every(p => !prevSelected.has(p.id));
|
||||
|
||||
|
||||
if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos)
|
||||
newSelected.add(idPermisoSeccion);
|
||||
// NO se marcan los hijos
|
||||
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0 ) {
|
||||
// Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos"
|
||||
newSelected.add(idPermisoSeccion); // Asegurar
|
||||
permisosDelModuloHijo.forEach(p => newSelected.add(p.id));
|
||||
} else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off
|
||||
newSelected.delete(idPermisoSeccion);
|
||||
permisosDelModuloHijo.forEach(p => newSelected.delete(p.id));
|
||||
}
|
||||
|
||||
} else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo
|
||||
if (asignadoViaCheckboxHijo) {
|
||||
newSelected.add(permisoId);
|
||||
const permisoSeccionPadre = permisosDisponibles.find(
|
||||
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
|
||||
);
|
||||
if (permisoSeccionPadre && !newSelected.has(permisoSeccionPadre.id)) {
|
||||
newSelected.add(permisoSeccionPadre.id); // Marcar padre si no estaba
|
||||
}
|
||||
} else { // Desmarcando un hijo
|
||||
newSelected.delete(permisoId);
|
||||
const permisoSeccionPadre = permisosDisponibles.find(
|
||||
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
|
||||
);
|
||||
if (permisoSeccionPadre) {
|
||||
const algunOtroHijoSeleccionado = permisosDelModuloHijo.some(p => p.id !== permisoId && newSelected.has(p.id));
|
||||
if (!algunOtroHijoSeleccionado && newSelected.has(permisoSeccionPadre.id)) {
|
||||
// Si era el último hijo y el padre estaba marcado, NO desmarcamos el padre automáticamente.
|
||||
// El estado indeterminate se encargará visualmente.
|
||||
// Si quisiéramos que se desmarque el padre, aquí iría: newSelected.delete(permisoSeccionPadre.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // Permiso sin módulo conceptual asociado (ej: "Permisos (Definición)")
|
||||
if (asignadoViaCheckboxHijo) {
|
||||
newSelected.add(permisoId);
|
||||
} else {
|
||||
newSelected.delete(permisoId);
|
||||
}
|
||||
}
|
||||
|
||||
if (successMessage) setSuccessMessage(null);
|
||||
if (error) setError(null);
|
||||
return newSelected;
|
||||
});
|
||||
// Limpiar mensajes al cambiar selección
|
||||
if (successMessage) setSuccessMessage(null);
|
||||
if (error) setError(null);
|
||||
};
|
||||
}, [permisosDisponibles, successMessage, error]);
|
||||
|
||||
|
||||
const handleGuardarCambios = async () => {
|
||||
// ... (sin cambios) ...
|
||||
if (!puedeAsignar || !perfil) return;
|
||||
setSaving(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
@@ -89,13 +201,12 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
permisosIds: Array.from(permisosSeleccionados)
|
||||
});
|
||||
setSuccessMessage('Permisos actualizados correctamente.');
|
||||
// Opcional: recargar datos, aunque el estado local ya está actualizado
|
||||
// cargarDatos();
|
||||
await cargarDatos();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: 'Error al guardar los permisos.';
|
||||
? err.response.data.message
|
||||
: 'Error al guardar los permisos.';
|
||||
setError(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -103,56 +214,54 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
if (error && !perfil) { // Si hay un error crítico al cargar el perfil
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeAsignar) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
if (!perfil) { // Si no hay error, pero el perfil es null después de cargar (no debería pasar si no hay error)
|
||||
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado.</Alert>;
|
||||
}
|
||||
if (error && !perfil) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||
}
|
||||
if (!puedeAsignar) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||
}
|
||||
if (!perfil && !loading) {
|
||||
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
||||
Volver a Perfiles
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
ID Perfil: {perfil?.id}
|
||||
</Typography>
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
||||
Volver a Perfiles
|
||||
</Button>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
ID Perfil: {perfil?.id}
|
||||
</Typography>
|
||||
{error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
||||
|
||||
<Paper sx={{ p: 2, mt: 2 }}>
|
||||
<PermisosChecklist
|
||||
permisosDisponibles={permisosDisponibles}
|
||||
permisosSeleccionados={permisosSeleccionados}
|
||||
onPermisoChange={handlePermisoChange}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
||||
onClick={handleGuardarCambios}
|
||||
disabled={saving || !puedeAsignar}
|
||||
>
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
<Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}>
|
||||
<PermisosChecklist
|
||||
permisosDisponibles={permisosDisponibles}
|
||||
permisosSeleccionados={permisosSeleccionados}
|
||||
onPermisoChange={handlePermisoChange}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
||||
onClick={handleGuardarCambios}
|
||||
disabled={saving || !puedeAsignar}
|
||||
>
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default AsignarPermisosAPerfilPage;
|
||||
@@ -6,6 +6,7 @@ import HomePage from '../pages/HomePage';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import MainLayout from '../layouts/MainLayout';
|
||||
import { Typography } from '@mui/material';
|
||||
import SectionProtectedRoute from './SectionProtectedRoute';
|
||||
|
||||
// Distribución
|
||||
import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage';
|
||||
@@ -38,6 +39,7 @@ import ContablesIndexPage from '../pages/Contables/ContablesIndexPage';
|
||||
import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage';
|
||||
import GestionarPagosDistribuidorPage from '../pages/Contables/GestionarPagosDistribuidorPage';
|
||||
import GestionarNotasCDPage from '../pages/Contables/GestionarNotasCDPage';
|
||||
import GestionarSaldosPage from '../pages/Contables/GestionarSaldosPage';
|
||||
|
||||
// Usuarios
|
||||
import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente
|
||||
@@ -69,10 +71,14 @@ import ReporteComparativaConsumoBobinasPage from '../pages/Reportes/ReporteCompa
|
||||
import ReporteCuentasDistribuidoresPage from '../pages/Reportes/ReporteCuentasDistribuidoresPage';
|
||||
import ReporteListadoDistribucionPage from '../pages/Reportes/ReporteListadoDistribucionPage';
|
||||
import ReporteControlDevolucionesPage from '../pages/Reportes/ReporteControlDevolucionesPage';
|
||||
import GestionarNovedadesCanillaPage from '../pages/Distribucion/GestionarNovedadesCanillaPage';
|
||||
import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage';
|
||||
import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage';
|
||||
|
||||
// Auditorias
|
||||
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
||||
|
||||
|
||||
// --- ProtectedRoute y PublicRoute SIN CAMBIOS ---
|
||||
const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@@ -107,7 +113,7 @@ const MainLayoutWrapper: React.FC = () => (
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes> {/* Un solo <Routes> de nivel superior */}
|
||||
<Routes>
|
||||
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
|
||||
|
||||
{/* Rutas Protegidas que usan el MainLayout */}
|
||||
@@ -123,13 +129,21 @@ const AppRoutes = () => {
|
||||
<Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */}
|
||||
|
||||
{/* Módulo de Distribución (anidado) */}
|
||||
<Route path="distribucion" element={<DistribucionIndexPage />}>
|
||||
<Route
|
||||
path="distribucion"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="SS001" sectionName="Distribución">
|
||||
<DistribucionIndexPage />
|
||||
</SectionProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="es-canillas" replace />} />
|
||||
<Route path="es-canillas" element={<GestionarEntradasSalidasCanillaPage />} />
|
||||
<Route path="control-devoluciones" element={<GestionarControlDevolucionesPage />} />
|
||||
<Route path="es-distribuidores" element={<GestionarEntradasSalidasDistPage />} />
|
||||
<Route path="salidas-otros-destinos" element={<GestionarSalidasOtrosDestinosPage />} />
|
||||
<Route path="canillas" element={<GestionarCanillitasPage />} />
|
||||
<Route path="canillas/:idCanilla/novedades" element={<GestionarNovedadesCanillaPage />} />
|
||||
<Route path="distribuidores" element={<GestionarDistribuidoresPage />} />
|
||||
<Route path="otros-destinos" element={<GestionarOtrosDestinosPage />} />
|
||||
<Route path="zonas" element={<GestionarZonasPage />} />
|
||||
@@ -146,15 +160,28 @@ const AppRoutes = () => {
|
||||
</Route>
|
||||
|
||||
{/* Módulo Contable (anidado) */}
|
||||
<Route path="contables" element={<ContablesIndexPage />}>
|
||||
<Route
|
||||
path="contables"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="SS002" sectionName="Contables">
|
||||
<ContablesIndexPage />
|
||||
</SectionProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="tipos-pago" replace />} />
|
||||
<Route path="tipos-pago" element={<GestionarTiposPagoPage />} />
|
||||
<Route path="pagos-distribuidores" element={<GestionarPagosDistribuidorPage />} />
|
||||
<Route path="notas-cd" element={<GestionarNotasCDPage />} />
|
||||
<Route path="gestion-saldos" element={<GestionarSaldosPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Impresión (anidado) */}
|
||||
<Route path="impresion" element={<ImpresionIndexPage />}>
|
||||
<Route path="impresion"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="SS003" sectionName="Impresión">
|
||||
<ImpresionIndexPage />
|
||||
</SectionProtectedRoute>}
|
||||
>
|
||||
<Route index element={<Navigate to="plantas" replace />} />
|
||||
<Route path="plantas" element={<GestionarPlantasPage />} />
|
||||
<Route path="tipos-bobina" element={<GestionarTiposBobinaPage />} />
|
||||
@@ -164,8 +191,13 @@ const AppRoutes = () => {
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Reportes */}
|
||||
<Route path="reportes" element={<ReportesIndexPage />}> {/* Página principal del módulo */}
|
||||
<Route index element={<Typography sx={{p:2}}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
|
||||
<Route path="reportes"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="SS004" sectionName="Reportes">
|
||||
<ReportesIndexPage />
|
||||
</SectionProtectedRoute>}
|
||||
>
|
||||
<Route index element={<Typography sx={{ p: 2 }}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
|
||||
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
|
||||
<Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
|
||||
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />
|
||||
@@ -181,10 +213,17 @@ const AppRoutes = () => {
|
||||
<Route path="cuentas-distribuidores" element={<ReporteCuentasDistribuidoresPage />} />
|
||||
<Route path="listado-distribucion-distribuidores" element={<ReporteListadoDistribucionPage />} />
|
||||
<Route path="control-devoluciones" element={<ReporteControlDevolucionesPage />} />
|
||||
<Route path="novedades-canillas" element={<ReporteNovedadesCanillasPage />} />
|
||||
<Route path="listado-distribucion-mensual" element={<ReporteListadoDistMensualPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Radios (anidado) */}
|
||||
<Route path="radios" element={<RadiosIndexPage />}>
|
||||
<Route path="radios"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="SS005" sectionName="Radios">
|
||||
<RadiosIndexPage />
|
||||
</SectionProtectedRoute>}
|
||||
>
|
||||
<Route index element={<Navigate to="ritmos" replace />} />
|
||||
<Route path="ritmos" element={<GestionarRitmosPage />} />
|
||||
<Route path="canciones" element={<GestionarCancionesPage />} />
|
||||
@@ -192,7 +231,12 @@ const AppRoutes = () => {
|
||||
</Route>
|
||||
|
||||
{/* Módulo de Usuarios (anidado) */}
|
||||
<Route path="usuarios" element={<UsuariosIndexPage />}>
|
||||
<Route path="usuarios"
|
||||
element={
|
||||
<SectionProtectedRoute requiredPermission="SS006" sectionName="Usuarios">
|
||||
<UsuariosIndexPage />
|
||||
</SectionProtectedRoute>}
|
||||
>
|
||||
<Route index element={<Navigate to="perfiles" replace />} /> {/* Redirigir a la primera subpestaña */}
|
||||
<Route path="perfiles" element={<GestionarPerfilesPage />} />
|
||||
<Route path="permisos" element={<GestionarPermisosPage />} />
|
||||
|
||||
52
Frontend/src/routes/SectionProtectedRoute.tsx
Normal file
52
Frontend/src/routes/SectionProtectedRoute.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
// src/routes/SectionProtectedRoute.tsx
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
|
||||
interface SectionProtectedRouteProps {
|
||||
requiredPermission: string;
|
||||
sectionName: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SectionProtectedRoute: React.FC<SectionProtectedRouteProps> = ({ requiredPermission, sectionName, children }) => {
|
||||
const { isAuthenticated, isLoading: authIsLoading } = useAuth(); // isLoading de AuthContext
|
||||
const { tienePermiso, isSuperAdmin, currentUser } = usePermissions();
|
||||
|
||||
if (authIsLoading) { // Esperar a que el AuthContext termine su carga inicial
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// En este punto, si está autenticado, currentUser debería estar disponible.
|
||||
// Si currentUser pudiera ser null aun estando autenticado (poco probable con tu AuthContext),
|
||||
// se necesitaría un manejo adicional o un spinner aquí.
|
||||
if (!currentUser) {
|
||||
// Esto sería un estado inesperado si isAuthenticated es true.
|
||||
// Podrías redirigir a login o mostrar un error genérico.
|
||||
console.error("SectionProtectedRoute: Usuario autenticado pero currentUser es null.");
|
||||
return <Navigate to="/login" replace />; // O un error más específico
|
||||
}
|
||||
|
||||
const canAccessSection = isSuperAdmin || tienePermiso(requiredPermission);
|
||||
|
||||
if (!canAccessSection) {
|
||||
console.error('SectionProtectedRoute: Usuario autenticado pero sin acceso a sección ', sectionName);
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
// Si children se proporciona (como <SectionProtectedRoute><IndexPage/></SectionProtectedRoute>), renderiza children.
|
||||
// Si no (como <Route element={<SectionProtectedRoute ... />} > <Route .../> </Route>), renderiza Outlet.
|
||||
return children ? <>{children}</> : <Outlet />;
|
||||
};
|
||||
|
||||
export default SectionProtectedRoute;
|
||||
31
Frontend/src/services/Contables/saldoService.ts
Normal file
31
Frontend/src/services/Contables/saldoService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import apiClient from '../apiClient';
|
||||
import type { SaldoGestionDto } from '../../models/dtos/Contables/SaldoGestionDto';
|
||||
import type { AjusteSaldoRequestDto } from '../../models/dtos/Contables/AjusteSaldoRequestDto';
|
||||
|
||||
interface GetSaldosParams {
|
||||
destino?: 'Distribuidores' | 'Canillas' | '';
|
||||
idDestino?: number | string; // Puede ser string si viene de un input antes de convertir
|
||||
idEmpresa?: number | string;
|
||||
}
|
||||
|
||||
const getAllSaldosGestion = async (filters?: GetSaldosParams): Promise<SaldoGestionDto[]> => {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters?.destino) params.destino = filters.destino;
|
||||
if (filters?.idDestino) params.idDestino = Number(filters.idDestino); // Asegurar número
|
||||
if (filters?.idEmpresa) params.idEmpresa = Number(filters.idEmpresa); // Asegurar número
|
||||
|
||||
const response = await apiClient.get<SaldoGestionDto[]>('/saldos', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const ajustarSaldo = async (data: AjusteSaldoRequestDto): Promise<SaldoGestionDto> => { // Esperamos el saldo actualizado
|
||||
const response = await apiClient.post<SaldoGestionDto>('/saldos/ajustar', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const saldoService = {
|
||||
getAllSaldosGestion,
|
||||
ajustarSaldo,
|
||||
};
|
||||
|
||||
export default saldoService;
|
||||
@@ -5,11 +5,17 @@ import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCani
|
||||
import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto';
|
||||
|
||||
|
||||
const getAllCanillas = async (nomApeFilter?: string, legajoFilter?: number, soloActivos?: boolean): Promise<CanillaDto[]> => {
|
||||
const getAllCanillas = async (
|
||||
nomApeFilter?: string,
|
||||
legajoFilter?: number,
|
||||
soloActivos?: boolean,
|
||||
esAccionistaFilter?: boolean // Asegúrate que esté aquí
|
||||
): Promise<CanillaDto[]> => {
|
||||
const params: Record<string, string | number | boolean> = {};
|
||||
if (nomApeFilter) params.nomApe = nomApeFilter;
|
||||
if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter;
|
||||
if (soloActivos !== undefined) params.soloActivos = soloActivos;
|
||||
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; // <<-- ¡CLAVE! Verifica esto.
|
||||
|
||||
const response = await apiClient.get<CanillaDto[]>('/canillas', { params });
|
||||
return response.data;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user