Implementación AnomalIA - Fix de dropdowns y permisos.
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GestionIntegral.Api.Dtos.Anomalia;
|
||||||
|
using GestionIntegral.Api.Services.Anomalia;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Controllers.Anomalia
|
||||||
|
{
|
||||||
|
[Route("api/alertas")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class AlertasController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAlertaService _alertaService;
|
||||||
|
|
||||||
|
public AlertasController(IAlertaService alertaService)
|
||||||
|
{
|
||||||
|
_alertaService = alertaService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/alertas
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<AlertaGenericaDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetAlertasNoLeidas()
|
||||||
|
{
|
||||||
|
var alertas = await _alertaService.ObtenerAlertasNoLeidasAsync();
|
||||||
|
return Ok(alertas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/alertas/{idAlerta}/marcar-leida
|
||||||
|
[HttpPost("{idAlerta:int}/marcar-leida")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> MarcarComoLeida(int idAlerta)
|
||||||
|
{
|
||||||
|
var (exito, error) = await _alertaService.MarcarComoLeidaAsync(idAlerta);
|
||||||
|
if (!exito)
|
||||||
|
{
|
||||||
|
return NotFound(new { message = error });
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/alertas/marcar-grupo-leido
|
||||||
|
[HttpPost("marcar-grupo-leido")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<IActionResult> MarcarGrupoLeido([FromBody] MarcarGrupoLeidoRequestDto request)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
var (exito, error) = await _alertaService.MarcarGrupoComoLeidoAsync(request.TipoAlerta, request.IdEntidad);
|
||||||
|
if (!exito)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = error });
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO para el cuerpo del request de marcar grupo
|
||||||
|
public class MarcarGrupoLeidoRequestDto
|
||||||
|
{
|
||||||
|
[System.ComponentModel.DataAnnotations.Required]
|
||||||
|
public string TipoAlerta { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[System.ComponentModel.DataAnnotations.Required]
|
||||||
|
public int IdEntidad { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,10 +50,19 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
|||||||
public async Task<IActionResult> GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? esAccionista, [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();
|
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||||
var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); // <<-- Pasa el parámetro
|
var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista);
|
||||||
return Ok(canillitas);
|
return Ok(canillitas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("dropdown")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<CanillaDropdownDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetAllDropdownCanillas([FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true)
|
||||||
|
{
|
||||||
|
var canillitas = await _canillaService.ObtenerTodosDropdownAsync(esAccionista, soloActivos);
|
||||||
|
return Ok(canillitas);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// GET: api/canillas/{id}
|
// GET: api/canillas/{id}
|
||||||
[HttpGet("{id:int}", Name = "GetCanillaById")]
|
[HttpGet("{id:int}", Name = "GetCanillaById")]
|
||||||
[ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -64,6 +64,23 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("dropdown")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<OtroDestinoDropdownDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IActionResult> GetAllOtrosDestinosDropdown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var destinos = await _otroDestinoService.ObtenerTodosDropdownAsync();
|
||||||
|
return Ok(destinos);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener Otros Destinos para dropdown.");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la lista de destinos.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET: api/otrosdestinos/{id}
|
// GET: api/otrosdestinos/{id}
|
||||||
[HttpGet("{id:int}", Name = "GetOtroDestinoById")]
|
[HttpGet("{id:int}", Name = "GetOtroDestinoById")]
|
||||||
[ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -67,6 +67,23 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/estadosbobina/dropdown
|
||||||
|
[HttpGet("dropdown")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<EstadoBobinaDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetAllDropdownEstadosBobina()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var estados = await _estadoBobinaService.ObtenerTodosDropdownAsync();
|
||||||
|
return Ok(estados);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener todos los Estados de Bobina.");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los estados de bobina.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET: api/estadosbobina/{id}
|
// GET: api/estadosbobina/{id}
|
||||||
[HttpGet("{id:int}", Name = "GetEstadoBobinaById")]
|
[HttpGet("{id:int}", Name = "GetEstadoBobinaById")]
|
||||||
[ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -62,6 +62,25 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/tiposbobina/dropdown
|
||||||
|
[HttpGet("dropdown")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<TipoBobinaDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IActionResult> GetAllTiposBobina()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tiposBobina = await _tipoBobinaService.ObtenerTodosDropdownAsync();
|
||||||
|
return Ok(tiposBobina);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener todos los Tipos de Bobina.");
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los tipos de bobina.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET: api/tiposbobina/{id}
|
// GET: api/tiposbobina/{id}
|
||||||
// Permiso: IB006 (Ver Tipos Bobinas)
|
// Permiso: IB006 (Ver Tipos Bobinas)
|
||||||
[HttpGet("{id:int}", Name = "GetTipoBobinaById")]
|
[HttpGet("{id:int}", Name = "GetTipoBobinaById")]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using GestionIntegral.Api.Dtos.Distribucion;
|
||||||
using GestionIntegral.Api.Models.Distribucion;
|
using GestionIntegral.Api.Models.Distribucion;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System; // Para Exception
|
using System; // Para Exception
|
||||||
@@ -25,7 +26,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
string? nomApeFilter,
|
string? nomApeFilter,
|
||||||
int? legajoFilter,
|
int? legajoFilter,
|
||||||
bool? esAccionista,
|
bool? esAccionista,
|
||||||
bool? soloActivos) // <<-- Parámetro aquí
|
bool? soloActivos)
|
||||||
{
|
{
|
||||||
using var connection = _connectionFactory.CreateConnection();
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
var sqlBuilder = new System.Text.StringBuilder(@"
|
var sqlBuilder = new System.Text.StringBuilder(@"
|
||||||
@@ -73,6 +74,37 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CanillaDropdownDto>> GetAllDropdownAsync(bool? esAccionista, bool? soloActivos)
|
||||||
|
{
|
||||||
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
|
var sqlBuilder = new System.Text.StringBuilder(@"
|
||||||
|
SELECT c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe
|
||||||
|
FROM dbo.dist_dtCanillas c
|
||||||
|
WHERE 1=1 ");
|
||||||
|
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
|
||||||
|
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;");
|
||||||
|
|
||||||
|
var result = await connection.QueryAsync<CanillaDropdownDto>(
|
||||||
|
sqlBuilder.ToString(),
|
||||||
|
parameters
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id)
|
public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using GestionIntegral.Api.Dtos.Distribucion;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||||
{
|
{
|
||||||
public interface ICanillaRepository
|
public interface ICanillaRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista);
|
Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista);
|
||||||
|
Task<IEnumerable<CanillaDropdownDto>> GetAllDropdownAsync(bool? esAccionista, bool? soloActivos);
|
||||||
Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id);
|
Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id);
|
||||||
Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla
|
Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla
|
||||||
Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction);
|
Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction);
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using GestionIntegral.Api.Dtos.Distribucion;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||||
{
|
{
|
||||||
public interface IOtroDestinoRepository
|
public interface IOtroDestinoRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter);
|
Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter);
|
||||||
|
Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync();
|
||||||
Task<OtroDestino?> GetByIdAsync(int id);
|
Task<OtroDestino?> GetByIdAsync(int id);
|
||||||
Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction);
|
Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction);
|
Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using GestionIntegral.Api.Dtos.Distribucion;
|
||||||
using GestionIntegral.Api.Models.Distribucion;
|
using GestionIntegral.Api.Models.Distribucion;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -44,6 +45,21 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync()
|
||||||
|
{
|
||||||
|
const string sql = "SELECT Id_Destino AS IdDestino, Nombre FROM dbo.dist_dtOtrosDestinos ORDER BY Nombre;";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
|
return await connection.QueryAsync<OtroDestinoDropdownDto>(sql);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener Otros Destinos para dropdown.");
|
||||||
|
return Enumerable.Empty<OtroDestinoDropdownDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<OtroDestino?> GetByIdAsync(int id)
|
public async Task<OtroDestino?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id";
|
const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using GestionIntegral.Api.Data.Repositories.Impresion;
|
using GestionIntegral.Api.Data.Repositories.Impresion;
|
||||||
|
using GestionIntegral.Api.Dtos.Impresion;
|
||||||
using GestionIntegral.Api.Models.Impresion;
|
using GestionIntegral.Api.Models.Impresion;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -45,6 +46,25 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync()
|
||||||
|
{
|
||||||
|
var sqlBuilder = new StringBuilder("SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion FROM dbo.bob_dtEstadosBobinas WHERE 1=1");
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
|
||||||
|
sqlBuilder.Append(" ORDER BY Denominacion;");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
|
return await connection.QueryAsync<EstadoBobinaDropdownDto>(sqlBuilder.ToString(), parameters);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener todos los Estados de Bobina.");
|
||||||
|
return Enumerable.Empty<EstadoBobinaDropdownDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<EstadoBobina?> GetByIdAsync(int id)
|
public async Task<EstadoBobina?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id";
|
const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id";
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Impresion;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using GestionIntegral.Api.Dtos.Impresion;
|
||||||
|
|
||||||
namespace GestionIntegral.Api.Data.Repositories.Impresion
|
namespace GestionIntegral.Api.Data.Repositories.Impresion
|
||||||
{
|
{
|
||||||
public interface IEstadoBobinaRepository
|
public interface IEstadoBobinaRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter);
|
Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter);
|
||||||
|
Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync();
|
||||||
Task<EstadoBobina?> GetByIdAsync(int id);
|
Task<EstadoBobina?> GetByIdAsync(int id);
|
||||||
Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction);
|
Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction);
|
Task<bool> UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion
|
|||||||
public interface ITipoBobinaRepository
|
public interface ITipoBobinaRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter);
|
Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter);
|
||||||
|
Task<IEnumerable<TipoBobina>> GetAllDropdownAsync();
|
||||||
Task<TipoBobina?> GetByIdAsync(int id);
|
Task<TipoBobina?> GetByIdAsync(int id);
|
||||||
Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction);
|
Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction);
|
||||||
Task<bool> UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction);
|
Task<bool> UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction);
|
||||||
|
|||||||
@@ -44,6 +44,24 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TipoBobina>> GetAllDropdownAsync()
|
||||||
|
{
|
||||||
|
var sqlBuilder = new StringBuilder("SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE 1=1");
|
||||||
|
|
||||||
|
sqlBuilder.Append(" ORDER BY Denominacion;");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = _connectionFactory.CreateConnection();
|
||||||
|
return await connection.QueryAsync<TipoBobina>(sqlBuilder.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener todos los Tipos de Bobina.");
|
||||||
|
return Enumerable.Empty<TipoBobina>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TipoBobina?> GetByIdAsync(int id)
|
public async Task<TipoBobina?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id";
|
const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id";
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Dtos.Anomalia
|
||||||
|
{
|
||||||
|
public class AlertaGenericaDto
|
||||||
|
{
|
||||||
|
public int IdAlerta { get; set; }
|
||||||
|
public DateTime FechaDeteccion { get; set; }
|
||||||
|
public string TipoAlerta { get; set; } = string.Empty;
|
||||||
|
public string Entidad { get; set; } = string.Empty;
|
||||||
|
public int IdEntidad { get; set; }
|
||||||
|
public string Mensaje { get; set; } = string.Empty;
|
||||||
|
public DateTime FechaAnomalia { get; set; }
|
||||||
|
public bool Leida { get; set; }
|
||||||
|
|
||||||
|
// Propiedades que pueden ser nulas porque no aplican a todos los tipos de alerta
|
||||||
|
public int? CantidadEnviada { get; set; }
|
||||||
|
public int? CantidadDevuelta { get; set; }
|
||||||
|
public decimal? PorcentajeDevolucion { get; set; }
|
||||||
|
|
||||||
|
// Podríamos añadir más propiedades opcionales en el futuro
|
||||||
|
// public string? NombreEntidad { get; set; } // Por ejemplo, el nombre del canillita
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||||
|
{
|
||||||
|
public class CanillaDropdownDto
|
||||||
|
{
|
||||||
|
public int IdCanilla { get; set; }
|
||||||
|
public int? Legajo { get; set; }
|
||||||
|
public string NomApe { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||||
|
{
|
||||||
|
public class OtroDestinoDropdownDto
|
||||||
|
{
|
||||||
|
public int IdDestino { get; set; }
|
||||||
|
public string Nombre { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace GestionIntegral.Api.Dtos.Distribucion
|
|||||||
{
|
{
|
||||||
public int IdPublicacion { get; set; }
|
public int IdPublicacion { get; set; }
|
||||||
public string Nombre { get; set; } = string.Empty;
|
public string Nombre { get; set; } = string.Empty;
|
||||||
public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null
|
public string NombreEmpresa { get; set; } = string.Empty;
|
||||||
|
public bool Habilitada { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace GestionIntegral.Api.Dtos.Impresion
|
||||||
|
{
|
||||||
|
public class EstadoBobinaDropdownDto
|
||||||
|
{
|
||||||
|
public int IdEstadoBobina { get; set; }
|
||||||
|
public string Denominacion { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ using GestionIntegral.Api.Data.Repositories.Reportes;
|
|||||||
using GestionIntegral.Api.Services.Reportes;
|
using GestionIntegral.Api.Services.Reportes;
|
||||||
using GestionIntegral.Api.Services.Pdf;
|
using GestionIntegral.Api.Services.Pdf;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using GestionIntegral.Api.Services.Anomalia;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -96,6 +97,8 @@ builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
|
|||||||
builder.Services.AddScoped<IReportesService, ReportesService>();
|
builder.Services.AddScoped<IReportesService, ReportesService>();
|
||||||
// QuestPDF
|
// QuestPDF
|
||||||
builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>();
|
builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>();
|
||||||
|
// Servicio de Alertas
|
||||||
|
builder.Services.AddScoped<IAlertaService, AlertaService>();
|
||||||
|
|
||||||
// --- SERVICIO DE HEALTH CHECKS ---
|
// --- SERVICIO DE HEALTH CHECKS ---
|
||||||
// Añadimos una comprobación específica para SQL Server.
|
// Añadimos una comprobación específica para SQL Server.
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using GestionIntegral.Api.Data;
|
||||||
|
using GestionIntegral.Api.Dtos.Anomalia;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Anomalia
|
||||||
|
{
|
||||||
|
public class AlertaService : IAlertaService
|
||||||
|
{
|
||||||
|
private readonly DbConnectionFactory _dbConnectionFactory;
|
||||||
|
private readonly ILogger<AlertaService> _logger;
|
||||||
|
|
||||||
|
public AlertaService(DbConnectionFactory dbConnectionFactory, ILogger<AlertaService> logger)
|
||||||
|
{
|
||||||
|
_dbConnectionFactory = dbConnectionFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync()
|
||||||
|
{
|
||||||
|
// Apunta a la nueva tabla genérica 'Sistema_Alertas'
|
||||||
|
var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||||
|
{
|
||||||
|
var alertas = await connection.QueryAsync<AlertaGenericaDto>(query);
|
||||||
|
return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener las alertas no leídas desde Sistema_Alertas.");
|
||||||
|
return Enumerable.Empty<AlertaGenericaDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta)
|
||||||
|
{
|
||||||
|
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||||
|
{
|
||||||
|
var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta });
|
||||||
|
if (result > 0)
|
||||||
|
{
|
||||||
|
return (true, null);
|
||||||
|
}
|
||||||
|
return (false, "La alerta no fue encontrada o ya estaba marcada.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al marcar la alerta {IdAlerta} como leída.", idAlerta);
|
||||||
|
return (false, "Error interno del servidor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Exito, string? Error)> MarcarGrupoComoLeidoAsync(string tipoAlerta, int idEntidad)
|
||||||
|
{
|
||||||
|
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||||
|
{
|
||||||
|
var result = await connection.ExecuteAsync(query, new { TipoAlerta = tipoAlerta, IdEntidad = idEntidad });
|
||||||
|
// No es un error si no se actualizan filas (puede que no hubiera ninguna para ese grupo)
|
||||||
|
_logger.LogInformation("Marcadas como leídas {Count} alertas para Tipo: {Tipo}, EntidadID: {IdEntidad}", result, tipoAlerta, idEntidad);
|
||||||
|
return (true, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al marcar grupo de alertas como leídas. Tipo: {Tipo}, EntidadID: {IdEntidad}", tipoAlerta, idEntidad);
|
||||||
|
return (false, "Error interno del servidor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GestionIntegral.Api.Dtos.Anomalia;
|
||||||
|
|
||||||
|
namespace GestionIntegral.Api.Services.Anomalia
|
||||||
|
{
|
||||||
|
public interface IAlertaService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene todas las alertas que no han sido marcadas como leídas.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Una colección de DTOs de alertas genéricas.</returns>
|
||||||
|
Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marca una alerta específica como leída.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idAlerta">El ID de la alerta a marcar.</param>
|
||||||
|
/// <returns>Una tupla indicando si la operación fue exitosa y un mensaje de error si falló.</returns>
|
||||||
|
Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marca como leídas todas las alertas de un mismo tipo y para una misma entidad.
|
||||||
|
/// (Ej: todas las alertas de "DevolucionAnomala" para el Canillita ID 45).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tipoAlerta">El tipo de alerta a marcar (ej. "DevolucionAnomala").</param>
|
||||||
|
/// <param name="idEntidad">El ID de la entidad afectada (ej. el IdCanilla).</param>
|
||||||
|
/// <returns>Una tupla indicando si la operación fue exitosa y un mensaje de error si falló.</returns>
|
||||||
|
Task<(bool Exito, string? Error)> MarcarGrupoComoLeidoAsync(string tipoAlerta, int idEntidad);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORREGIDO: MapToDto ahora acepta una tupla con tipos anulables
|
|
||||||
private CanillaDto? MapToDto((Canilla? Canilla, string? NombreZona, string? NombreEmpresa) data)
|
private CanillaDto? MapToDto((Canilla? Canilla, string? NombreZona, string? NombreEmpresa) data)
|
||||||
{
|
{
|
||||||
if (data.Canilla == null) return null;
|
if (data.Canilla == null) return null;
|
||||||
@@ -62,10 +61,14 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CanillaDropdownDto>> ObtenerTodosDropdownAsync(bool? esAccionista, bool? soloActivos)
|
||||||
|
{
|
||||||
|
return await _canillaRepository.GetAllDropdownAsync(esAccionista, soloActivos) ?? Enumerable.Empty<CanillaDropdownDto>();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CanillaDto?> ObtenerPorIdAsync(int id)
|
public async Task<CanillaDto?> ObtenerPorIdAsync(int id)
|
||||||
{
|
{
|
||||||
var data = await _canillaRepository.GetByIdAsync(id);
|
var data = await _canillaRepository.GetByIdAsync(id);
|
||||||
// MapToDto ahora devuelve CanillaDto? así que esto es correcto
|
|
||||||
return MapToDto(data);
|
return MapToDto(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +92,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORREGIDO: Usar directamente el valor booleano
|
|
||||||
if (createDto.Accionista == true && createDto.Empresa != 0)
|
if (createDto.Accionista == true && createDto.Empresa != 0)
|
||||||
{
|
{
|
||||||
return (null, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0).");
|
return (null, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0).");
|
||||||
@@ -287,6 +289,6 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
FechaMod = h.Historial.FechaMod,
|
FechaMod = h.Historial.FechaMod,
|
||||||
TipoMod = h.Historial.TipoMod
|
TipoMod = h.Historial.TipoMod
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
public interface ICanillaService
|
public interface ICanillaService
|
||||||
{
|
{
|
||||||
Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos);
|
Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos);
|
||||||
|
Task<IEnumerable<CanillaDropdownDto>> ObtenerTodosDropdownAsync(bool? esAccionista, bool? soloActivos);
|
||||||
Task<CanillaDto?> ObtenerPorIdAsync(int id);
|
Task<CanillaDto?> ObtenerPorIdAsync(int id);
|
||||||
Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario);
|
Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
public interface IOtroDestinoService
|
public interface IOtroDestinoService
|
||||||
{
|
{
|
||||||
Task<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter);
|
Task<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter);
|
||||||
|
Task<IEnumerable<OtroDestinoDropdownDto>> ObtenerTodosDropdownAsync();
|
||||||
Task<OtroDestinoDto?> ObtenerPorIdAsync(int id);
|
Task<OtroDestinoDto?> ObtenerPorIdAsync(int id);
|
||||||
Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, int idUsuario);
|
Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateOtroDestinoDto updateDto, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateOtroDestinoDto updateDto, int idUsuario);
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
return destinos.Select(MapToDto);
|
return destinos.Select(MapToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<OtroDestinoDropdownDto>> ObtenerTodosDropdownAsync()
|
||||||
|
{
|
||||||
|
return await _otroDestinoRepository.GetAllDropdownAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<OtroDestinoDto?> ObtenerPorIdAsync(int id)
|
public async Task<OtroDestinoDto?> ObtenerPorIdAsync(int id)
|
||||||
{
|
{
|
||||||
var destino = await _otroDestinoRepository.GetByIdAsync(id);
|
var destino = await _otroDestinoRepository.GetByIdAsync(id);
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
|||||||
{
|
{
|
||||||
IdPublicacion = d.Publicacion!.IdPublicacion, // Usar ! si estás seguro que no es null después del Where
|
IdPublicacion = d.Publicacion!.IdPublicacion, // Usar ! si estás seguro que no es null después del Where
|
||||||
Nombre = d.Publicacion!.Nombre,
|
Nombre = d.Publicacion!.Nombre,
|
||||||
|
NombreEmpresa = d.NombreEmpresa ?? "Empresa Desconocida",
|
||||||
Habilitada = d.Publicacion!.Habilitada ?? true // Si necesitas filtrar por esto
|
Habilitada = d.Publicacion!.Habilitada ?? true // Si necesitas filtrar por esto
|
||||||
})
|
})
|
||||||
.OrderBy(p => p.Nombre)
|
.OrderBy(p => p.Nombre)
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
return estadosBobina.Select(MapToDto);
|
return estadosBobina.Select(MapToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<EstadoBobinaDropdownDto>> ObtenerTodosDropdownAsync()
|
||||||
|
{
|
||||||
|
var estadosBobina = await _estadoBobinaRepository.GetAllDropdownAsync();
|
||||||
|
return estadosBobina;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id)
|
public async Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id)
|
||||||
{
|
{
|
||||||
var estadoBobina = await _estadoBobinaRepository.GetByIdAsync(id);
|
var estadoBobina = await _estadoBobinaRepository.GetByIdAsync(id);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
public interface IEstadoBobinaService
|
public interface IEstadoBobinaService
|
||||||
{
|
{
|
||||||
Task<IEnumerable<EstadoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
|
Task<IEnumerable<EstadoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
|
||||||
|
Task<IEnumerable<EstadoBobinaDropdownDto>> ObtenerTodosDropdownAsync();
|
||||||
Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id);
|
Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id);
|
||||||
Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, int idUsuario);
|
Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEstadoBobinaDto updateDto, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEstadoBobinaDto updateDto, int idUsuario);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
public interface ITipoBobinaService
|
public interface ITipoBobinaService
|
||||||
{
|
{
|
||||||
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
|
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
|
||||||
|
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosDropdownAsync();
|
||||||
Task<TipoBobinaDto?> ObtenerPorIdAsync(int id);
|
Task<TipoBobinaDto?> ObtenerPorIdAsync(int id);
|
||||||
Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, int idUsuario);
|
Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, int idUsuario);
|
||||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoBobinaDto updateDto, int idUsuario);
|
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoBobinaDto updateDto, int idUsuario);
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ namespace GestionIntegral.Api.Services.Impresion
|
|||||||
return tiposBobina.Select(MapToDto);
|
return tiposBobina.Select(MapToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TipoBobinaDto>> ObtenerTodosDropdownAsync()
|
||||||
|
{
|
||||||
|
var tiposBobina = await _tipoBobinaRepository.GetAllDropdownAsync();
|
||||||
|
return tiposBobina.Select(MapToDto);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TipoBobinaDto?> ObtenerPorIdAsync(int id)
|
public async Task<TipoBobinaDto?> ObtenerPorIdAsync(int id)
|
||||||
{
|
{
|
||||||
var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(id);
|
var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(id);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
|||||||
if (moduloLower.includes("impresión tiradas") ||
|
if (moduloLower.includes("impresión tiradas") ||
|
||||||
moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas"
|
moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas"
|
||||||
moduloLower.includes("impresión plantas") ||
|
moduloLower.includes("impresión plantas") ||
|
||||||
|
moduloLower.includes("estados bobinas") ||
|
||||||
moduloLower.includes("tipos bobinas")) { // Añadido explícitamente
|
moduloLower.includes("tipos bobinas")) { // Añadido explícitamente
|
||||||
return "Impresión";
|
return "Impresión";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,15 +228,12 @@ const UsuarioFormModal: React.FC<UsuarioFormModalProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}> {/* Fila 5 (Checkboxes) */}
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}>
|
||||||
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
|
|
||||||
<FormControlLabel control={<Checkbox checked={supAdmin} onChange={(e) => setSupAdmin(e.target.checked)} disabled={loading}/>} label="Super Administrador" />
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
|
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
|
||||||
<FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" />
|
<FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box> {/* Fin contenedor principal de campos */}
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react';
|
import React, { createContext, useState, useContext, type ReactNode, useEffect, useCallback } from 'react';
|
||||||
import type { LoginResponseDto } from '../models/dtos/Usuarios/LoginResponseDto';
|
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import { getAlertas, marcarAlertaLeida, marcarGrupoComoLeido, type AlertaGenericaDto } from '../services/Anomalia/alertaService';
|
||||||
|
|
||||||
// Interfaz para los datos del usuario que guardaremos en el contexto
|
|
||||||
export interface UserContextData {
|
export interface UserContextData {
|
||||||
userId: number;
|
userId: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -11,33 +10,37 @@ export interface UserContextData {
|
|||||||
debeCambiarClave: boolean;
|
debeCambiarClave: boolean;
|
||||||
perfil: string;
|
perfil: string;
|
||||||
idPerfil: number;
|
idPerfil: number;
|
||||||
permissions: string[]; // Guardamos los codAcc
|
permissions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interfaz para el payload decodificado del JWT
|
|
||||||
interface DecodedJwtPayload {
|
interface DecodedJwtPayload {
|
||||||
sub: string; // User ID (viene como string)
|
sub: string;
|
||||||
name: string; // Username
|
name: string;
|
||||||
given_name?: string; // Nombre (estándar, pero verifica tu token)
|
given_name?: string;
|
||||||
family_name?: string; // Apellido (estándar, pero verifica tu token)
|
family_name?: string;
|
||||||
role: string | string[]; // Puede ser uno o varios roles
|
role: string | string[];
|
||||||
perfil: string;
|
perfil: string;
|
||||||
idPerfil: string; // (viene como string)
|
idPerfil: string;
|
||||||
debeCambiarClave: string; // (viene como string "True" o "False")
|
debeCambiarClave: string;
|
||||||
permission?: string | string[]; // Nuestros claims de permiso (codAcc)
|
permission?: string | string[];
|
||||||
[key: string]: any; // Para otros claims
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
user: UserContextData | null; // Usar el tipo extendido
|
user: UserContextData | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
alertas: AlertaGenericaDto[];
|
||||||
showForcedPasswordChangeModal: boolean;
|
showForcedPasswordChangeModal: boolean;
|
||||||
isPasswordChangeForced: boolean;
|
isPasswordChangeForced: boolean;
|
||||||
|
|
||||||
|
marcarAlertaComoLeida: (idAlerta: number) => Promise<void>;
|
||||||
|
marcarGrupoDeAlertasLeido: (tipoAlerta: string, idEntidad: number) => Promise<void>;
|
||||||
|
|
||||||
setShowForcedPasswordChangeModal: (show: boolean) => void;
|
setShowForcedPasswordChangeModal: (show: boolean) => void;
|
||||||
passwordChangeCompleted: () => void;
|
passwordChangeCompleted: () => void;
|
||||||
login: (apiLoginResponse: LoginResponseDto) => void; // Recibe el DTO de la API
|
login: (apiLoginResponse: any) => void; // DTO no definido aquí, usamos any
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,24 +53,57 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false);
|
const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false);
|
||||||
const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false);
|
const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false);
|
||||||
|
const [alertas, setAlertas] = useState<AlertaGenericaDto[]>([]);
|
||||||
|
|
||||||
const processTokenAndSetUser = (jwtToken: string) => {
|
const fetchAlertas = useCallback(async (currentUser: UserContextData | null) => {
|
||||||
|
if (currentUser && (currentUser.esSuperAdmin || currentUser.permissions.includes('AL001'))) {
|
||||||
|
try {
|
||||||
|
const data = await getAlertas();
|
||||||
|
setAlertas(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al obtener alertas en AuthContext:", error);
|
||||||
|
setAlertas([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAlertas([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const marcarAlertaComoLeida = async (idAlerta: number) => {
|
||||||
|
try {
|
||||||
|
await marcarAlertaLeida(idAlerta);
|
||||||
|
await fetchAlertas(user); // Refresca el estado global
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al marcar alerta como leída:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const marcarGrupoDeAlertasLeido = async (tipoAlerta: string, idEntidad: number) => {
|
||||||
|
try {
|
||||||
|
await marcarGrupoComoLeido({ tipoAlerta, idEntidad });
|
||||||
|
await fetchAlertas(user); // Refresca el estado global
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error al marcar grupo ${tipoAlerta}/${idEntidad} como leído:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
setToken(null);
|
||||||
|
setUser(null);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setShowForcedPasswordChangeModal(false);
|
||||||
|
setIsPasswordChangeForced(false);
|
||||||
|
setAlertas([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const processTokenAndSetUser = useCallback((jwtToken: string) => {
|
||||||
try {
|
try {
|
||||||
const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken);
|
const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken);
|
||||||
|
|
||||||
// Verificar expiración (opcional, pero buena práctica aquí también)
|
|
||||||
const currentTime = Date.now() / 1000;
|
const currentTime = Date.now() / 1000;
|
||||||
if (decodedToken.exp && decodedToken.exp < currentTime) {
|
if (decodedToken.exp && decodedToken.exp < currentTime) {
|
||||||
console.warn("Token expirado al procesar.");
|
logout(); return;
|
||||||
logout(); // Llama a logout que limpia todo
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let permissions: string[] = [];
|
|
||||||
if (decodedToken.permission) {
|
|
||||||
permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission];
|
|
||||||
}
|
|
||||||
|
|
||||||
const userForContext: UserContextData = {
|
const userForContext: UserContextData = {
|
||||||
userId: parseInt(decodedToken.sub, 10),
|
userId: parseInt(decodedToken.sub, 10),
|
||||||
username: decodedToken.name,
|
username: decodedToken.name,
|
||||||
@@ -75,27 +111,23 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")),
|
esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")),
|
||||||
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
|
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
|
||||||
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
|
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
|
||||||
permissions: permissions,
|
permissions: Array.isArray(decodedToken.permission) ? decodedToken.permission : (decodedToken.permission ? [decodedToken.permission] : []),
|
||||||
perfil: decodedToken.perfil || 'Usuario' // Asignar un valor por defecto si no existe
|
perfil: decodedToken.perfil || 'Usuario'
|
||||||
};
|
};
|
||||||
|
|
||||||
setToken(jwtToken);
|
setToken(jwtToken);
|
||||||
setUser(userForContext);
|
setUser(userForContext);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
localStorage.setItem('authToken', jwtToken);
|
localStorage.setItem('authToken', jwtToken);
|
||||||
localStorage.setItem('authUser', JSON.stringify(userForContext)); // Guardar el usuario procesado
|
|
||||||
|
|
||||||
// Lógica para el modal de cambio de clave
|
|
||||||
if (userForContext.debeCambiarClave) {
|
if (userForContext.debeCambiarClave) {
|
||||||
setShowForcedPasswordChangeModal(true);
|
setShowForcedPasswordChangeModal(true);
|
||||||
setIsPasswordChangeForced(true);
|
setIsPasswordChangeForced(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al decodificar o procesar token:", error);
|
console.error("Error al decodificar token:", error);
|
||||||
logout(); // Limpiar estado si el token es inválido
|
logout();
|
||||||
}
|
}
|
||||||
};
|
}, [logout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -104,20 +136,18 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
processTokenAndSetUser(storedToken);
|
processTokenAndSetUser(storedToken);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, []);
|
}, [processTokenAndSetUser]);
|
||||||
|
|
||||||
const login = (apiLoginResponse: LoginResponseDto) => {
|
useEffect(() => {
|
||||||
processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido
|
if (user && isAuthenticated) {
|
||||||
};
|
fetchAlertas(user);
|
||||||
|
const intervalId = setInterval(() => fetchAlertas(user), 300000); // Refresca cada 5 mins
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
}, [user, isAuthenticated, fetchAlertas]);
|
||||||
|
|
||||||
const logout = () => {
|
const login = (apiLoginResponse: any) => {
|
||||||
localStorage.removeItem('authToken');
|
processTokenAndSetUser(apiLoginResponse.token);
|
||||||
localStorage.removeItem('authUser');
|
|
||||||
setToken(null);
|
|
||||||
setUser(null);
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
setShowForcedPasswordChangeModal(false);
|
|
||||||
setIsPasswordChangeForced(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const passwordChangeCompleted = () => {
|
const passwordChangeCompleted = () => {
|
||||||
@@ -138,6 +168,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
<AuthContext.Provider value={{
|
<AuthContext.Provider value={{
|
||||||
isAuthenticated, user, token, isLoading,
|
isAuthenticated, user, token, isLoading,
|
||||||
showForcedPasswordChangeModal, isPasswordChangeForced,
|
showForcedPasswordChangeModal, isPasswordChangeForced,
|
||||||
|
alertas, marcarAlertaComoLeida, marcarGrupoDeAlertasLeido,
|
||||||
setShowForcedPasswordChangeModal, passwordChangeCompleted,
|
setShowForcedPasswordChangeModal, passwordChangeCompleted,
|
||||||
login, logout
|
login, logout
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// src/layouts/MainLayout.tsx
|
import React, { type ReactNode, useState, useEffect, useMemo } from 'react';
|
||||||
import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo
|
|
||||||
from 'react';
|
|
||||||
import {
|
import {
|
||||||
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
|
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
|
||||||
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider,
|
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider,
|
||||||
Button
|
Button, Badge
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||||
import LockResetIcon from '@mui/icons-material/LockReset';
|
import LockResetIcon from '@mui/icons-material/LockReset';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
|
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
@@ -18,6 +17,16 @@ interface MainLayoutProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Helper para dar nombres legibles a los tipos de alerta ---
|
||||||
|
const getTipoAlertaLabel = (tipoAlerta: string): string => {
|
||||||
|
switch (tipoAlerta) {
|
||||||
|
case 'DevolucionAnomala': return 'Devoluciones Anómalas';
|
||||||
|
case 'ComportamientoSistema': return 'Anomalías del Sistema';
|
||||||
|
case 'FaltaDeDatos': return 'Falta de Datos';
|
||||||
|
default: return tipoAlerta;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Definición original de módulos
|
// Definición original de módulos
|
||||||
const allAppModules = [
|
const allAppModules = [
|
||||||
{ label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible
|
{ label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible
|
||||||
@@ -31,22 +40,36 @@ const allAppModules = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||||
|
// Obtenemos todo lo necesario del AuthContext, INCLUYENDO LAS ALERTAS
|
||||||
const {
|
const {
|
||||||
user, // user ya está disponible aquí
|
user, logout, isAuthenticated, isPasswordChangeForced,
|
||||||
logout,
|
showForcedPasswordChangeModal, setShowForcedPasswordChangeModal,
|
||||||
isAuthenticated,
|
passwordChangeCompleted,
|
||||||
isPasswordChangeForced,
|
alertas
|
||||||
showForcedPasswordChangeModal,
|
|
||||||
setShowForcedPasswordChangeModal,
|
|
||||||
passwordChangeCompleted
|
|
||||||
} = useAuth();
|
} = useAuth();
|
||||||
|
|
||||||
const { tienePermiso, isSuperAdmin } = usePermissions(); // <<--- OBTENER HOOK DE PERMISOS
|
// El resto de los hooks locales no cambian
|
||||||
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
||||||
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null);
|
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null);
|
||||||
|
const [anchorElAlertasMenu, setAnchorElAlertasMenu] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
// --- Agrupación de alertas para el menú ---
|
||||||
|
const gruposDeAlertas = useMemo(() => {
|
||||||
|
if (!alertas || !Array.isArray(alertas)) return [];
|
||||||
|
|
||||||
|
const groups = alertas.reduce((acc, alerta) => {
|
||||||
|
const label = getTipoAlertaLabel(alerta.tipoAlerta);
|
||||||
|
acc[label] = (acc[label] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
return Object.entries(groups); // Devuelve [['Devoluciones Anómalas', 5], ...]
|
||||||
|
}, [alertas]);
|
||||||
|
|
||||||
|
const numAlertas = alertas.length;
|
||||||
|
|
||||||
const accessibleModules = useMemo(() => {
|
const accessibleModules = useMemo(() => {
|
||||||
if (!isAuthenticated) return [];
|
if (!isAuthenticated) return [];
|
||||||
@@ -92,6 +115,17 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
|||||||
setAnchorElUserMenu(null);
|
setAnchorElUserMenu(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handlers para el nuevo menú de alertas
|
||||||
|
const handleOpenAlertasMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElAlertasMenu(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseAlertasMenu = () => {
|
||||||
|
setAnchorElAlertasMenu(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToAlertas = () => { navigate('/anomalias/alertas'); handleCloseAlertasMenu(); };
|
||||||
|
|
||||||
const handleChangePasswordClick = () => {
|
const handleChangePasswordClick = () => {
|
||||||
setShowForcedPasswordChangeModal(true);
|
setShowForcedPasswordChangeModal(true);
|
||||||
handleCloseUserMenu();
|
handleCloseUserMenu();
|
||||||
@@ -133,7 +167,6 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Si no hay módulos accesibles después del login (y no es el cambio de clave forzado)
|
// 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.
|
// 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".
|
// Deberías redirigir a login o mostrar un mensaje de "Sin acceso".
|
||||||
@@ -162,6 +195,37 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
|||||||
)}
|
)}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<>
|
<>
|
||||||
|
<IconButton onClick={handleOpenAlertasMenu} color="inherit">
|
||||||
|
<Badge badgeContent={numAlertas} color="error">
|
||||||
|
<NotificationsIcon />
|
||||||
|
</Badge>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
id="alertas-menu"
|
||||||
|
anchorEl={anchorElAlertasMenu}
|
||||||
|
open={Boolean(anchorElAlertasMenu)}
|
||||||
|
onClose={() => setAnchorElAlertasMenu(null)}
|
||||||
|
>
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemText primary={`Tienes ${numAlertas} alertas pendientes.`} />
|
||||||
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{gruposDeAlertas.map(([label, count]) => (
|
||||||
|
<MenuItem key={label} onClick={handleNavigateToAlertas}>
|
||||||
|
<ListItemIcon><Badge badgeContent={count} color="error" sx={{mr: 2}} /></ListItemIcon>
|
||||||
|
<ListItemText>{label}</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{numAlertas > 0 && <Divider />}
|
||||||
|
|
||||||
|
<MenuItem onClick={handleNavigateToAlertas}>
|
||||||
|
<ListItemText sx={{textAlign: 'center'}}>Ver Todas las Alertas</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
aria-label="Cuenta del usuario"
|
aria-label="Cuenta del usuario"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface CanillaDropdownDto {
|
||||||
|
idCanilla: number;
|
||||||
|
legajo?: number | null;
|
||||||
|
nomApe: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface OtroDestinoDropdownDto {
|
||||||
|
idDestino: number;
|
||||||
|
nombre: string;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface PublicacionDropdownDto {
|
export interface PublicacionDropdownDto {
|
||||||
idPublicacion: number;
|
idPublicacion: number;
|
||||||
nombre: string;
|
nombre: string;
|
||||||
|
nombreEmpresa: string;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface EstadoBobinaDropdownDto {
|
||||||
|
idEstadoBobina: number;
|
||||||
|
denominacion: string;
|
||||||
|
}
|
||||||
129
Frontend/src/pages/Anomalia/AlertasPage.tsx
Normal file
129
Frontend/src/pages/Anomalia/AlertasPage.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||||
|
import { Button, Box, Typography, Paper, Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { esES } from '@mui/x-data-grid/locales';
|
||||||
|
import type { AlertaGenericaDto } from '../../services/Anomalia/alertaService';
|
||||||
|
|
||||||
|
const getTipoAlertaLabel = (tipoAlerta: string): string => {
|
||||||
|
switch (tipoAlerta) {
|
||||||
|
case 'DevolucionAnomala': return 'Devoluciones Anómalas';
|
||||||
|
case 'ConsumoBobinaExcesivo': return 'Consumo de Bobinas Anómalo';
|
||||||
|
case 'ComportamientoSistema': return 'Anomalías Generales del Sistema';
|
||||||
|
case 'FaltaDeDatos': return 'Falta de Registros Críticos';
|
||||||
|
default: return tipoAlerta;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlertasPage: React.FC = () => {
|
||||||
|
const { alertas, marcarAlertaComoLeida, marcarGrupoDeAlertasLeido, isLoading } = useAuth();
|
||||||
|
|
||||||
|
const gruposPorTipo = useMemo(() => {
|
||||||
|
if (!Array.isArray(alertas)) return [];
|
||||||
|
return alertas.reduce((acc, alerta) => {
|
||||||
|
(acc[alerta.tipoAlerta] = acc[alerta.tipoAlerta] || []).push(alerta);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, AlertaGenericaDto[]>);
|
||||||
|
}, [alertas]);
|
||||||
|
|
||||||
|
const getColumnsForType = (tipoAlerta: string): GridColDef[] => {
|
||||||
|
const baseColumns: GridColDef[] = [
|
||||||
|
{ field: 'fechaAnomalia', headerName: 'Fecha Evento', width: 150, valueFormatter: (value) => new Date(value as string).toLocaleDateString('es-AR') },
|
||||||
|
{ field: 'mensaje', headerName: 'Descripción', flex: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Columnas específicas para 'DevolucionAnomala'
|
||||||
|
if (tipoAlerta === 'DevolucionAnomala') {
|
||||||
|
baseColumns.push(
|
||||||
|
{ field: 'cantidadEnviada', headerName: 'Llevados', width: 120 },
|
||||||
|
{ field: 'cantidadDevuelta', headerName: 'Devueltos', width: 120 },
|
||||||
|
{ field: 'porcentajeDevolucion', headerName: '% Dev.', width: 120, valueFormatter: (value) => `${Number(value).toFixed(2)}%` }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
baseColumns.push({
|
||||||
|
field: 'actions',
|
||||||
|
headerName: 'Acciones',
|
||||||
|
width: 150,
|
||||||
|
sortable: false,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
// Llamamos a la función del contexto para marcar una SOLA alerta
|
||||||
|
onClick={() => marcarAlertaComoLeida(params.row.idAlerta)}
|
||||||
|
>
|
||||||
|
Marcar Leída
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return baseColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>Centro de Alertas del Sistema</Typography>
|
||||||
|
|
||||||
|
{Object.entries(gruposPorTipo).map(([tipoAlerta, alertasDelGrupo]) => {
|
||||||
|
const gruposPorEntidad = alertasDelGrupo.reduce((acc, alerta) => {
|
||||||
|
(acc[alerta.idEntidad] = acc[alerta.idEntidad] || []).push(alerta);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, AlertaGenericaDto[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion key={tipoAlerta} defaultExpanded>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Typography variant="h6">{getTipoAlertaLabel(tipoAlerta)} ({alertasDelGrupo.length})</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||||
|
{Object.entries(gruposPorEntidad).map(([idEntidad, alertasDeEntidad]) => {
|
||||||
|
const primeraAlerta = alertasDeEntidad[0];
|
||||||
|
// Para obtener un nombre de canillita legible en el título del grupo
|
||||||
|
const nombreEntidad = primeraAlerta.entidad === 'Canillita'
|
||||||
|
? primeraAlerta.mensaje.match(/'([^']+)'/)?.[1] || `ID ${idEntidad}`
|
||||||
|
: `ID ${idEntidad}`;
|
||||||
|
|
||||||
|
const tituloGrupo = primeraAlerta.entidad === 'Sistema'
|
||||||
|
? 'Alertas Generales del Sistema'
|
||||||
|
: `${primeraAlerta.entidad}: ${nombreEntidad}`;
|
||||||
|
|
||||||
|
const rows = alertasDeEntidad.map(a => ({ ...a, id: a.idAlerta }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper key={idEntidad} variant="outlined" sx={{ p: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1, flexWrap: 'wrap', gap: 1 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{fontWeight: 'bold'}}>{tituloGrupo} ({alertasDeEntidad.length} alertas)</Typography>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => marcarGrupoDeAlertasLeido(tipoAlerta, Number(idEntidad))}>
|
||||||
|
Marcar todas como leídas
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ height: 300, width: '100%' }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={getColumnsForType(tipoAlerta)}
|
||||||
|
loading={isLoading}
|
||||||
|
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{alertas.length === 0 && !isLoading && (
|
||||||
|
<Typography sx={{mt: 3, textAlign: 'center', fontStyle: 'italic'}}>No hay alertas pendientes.</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlertasPage;
|
||||||
@@ -16,13 +16,14 @@ import empresaService from '../../services/Distribucion/empresaService';
|
|||||||
import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
|
import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
|
||||||
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
|
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
|
||||||
import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto';
|
import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto';
|
||||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||||
|
|
||||||
import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal';
|
import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const GestionarControlDevolucionesPage: React.FC = () => {
|
const GestionarControlDevolucionesPage: React.FC = () => {
|
||||||
|
// ... (estados sin cambios) ...
|
||||||
const [controles, setControles] = useState<ControlDevolucionesDto[]>([]);
|
const [controles, setControles] = useState<ControlDevolucionesDto[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -32,8 +33,8 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||||
|
|
||||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(true); // << CAMBIO: Iniciar en true
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null);
|
const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null);
|
||||||
@@ -47,42 +48,58 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
const puedeVer = isSuperAdmin || tienePermiso("CD001");
|
const puedeVer = isSuperAdmin || tienePermiso("CD001");
|
||||||
const puedeCrear = isSuperAdmin || tienePermiso("CD002");
|
const puedeCrear = isSuperAdmin || tienePermiso("CD002");
|
||||||
const puedeModificar = isSuperAdmin || tienePermiso("CD003");
|
const puedeModificar = isSuperAdmin || tienePermiso("CD003");
|
||||||
const puedeEliminar = isSuperAdmin || tienePermiso("CD003");
|
// << CAMBIO: Permiso de eliminar debe ser diferente
|
||||||
|
const puedeEliminar = isSuperAdmin || tienePermiso("CD004"); // Asumiendo que CD004 es para eliminar
|
||||||
|
|
||||||
// CORREGIDO: Función para formatear la fecha
|
// ... (formatDate sin cambios) ...
|
||||||
const formatDate = (dateString?: string | null): string => {
|
const formatDate = (dateString?: string | null): string => {
|
||||||
if (!dateString) return '-';
|
if (!dateString) return '-';
|
||||||
// Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..."
|
const datePart = dateString.split('T')[0];
|
||||||
const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD
|
|
||||||
const parts = datePart.split('-');
|
const parts = datePart.split('-');
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD
|
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||||
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
|
|
||||||
}
|
}
|
||||||
return datePart; // Fallback si el formato no es el esperado
|
return datePart;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const fetchFiltersDropdownData = useCallback(async () => {
|
const fetchFiltersDropdownData = useCallback(async () => {
|
||||||
|
// << CAMBIO: Guardián de permisos para la carga de filtros
|
||||||
|
if (!puedeVer) {
|
||||||
|
setError("No tiene permiso para ver esta sección.");
|
||||||
|
setLoading(false); // Detiene el spinner principal
|
||||||
|
setLoadingFiltersDropdown(false); // Detiene el spinner de filtros
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
try {
|
try {
|
||||||
const empresasData = await empresaService.getAllEmpresas();
|
const empresasData = await empresaService.getEmpresasDropdown();
|
||||||
setEmpresas(empresasData);
|
setEmpresas(empresasData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error cargando empresas para filtro:", err);
|
console.error("Error cargando empresas para filtro:", err);
|
||||||
|
// El error principal se manejará en cargarControles si también falla
|
||||||
setError("Error al cargar opciones de filtro.");
|
setError("Error al cargar opciones de filtro.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingFiltersDropdown(false);
|
setLoadingFiltersDropdown(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia
|
||||||
|
|
||||||
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
useEffect(() => {
|
||||||
|
fetchFiltersDropdownData();
|
||||||
|
}, [fetchFiltersDropdownData]);
|
||||||
|
|
||||||
const cargarControles = useCallback(async () => {
|
const cargarControles = useCallback(async () => {
|
||||||
|
// El guardián aquí ya estaba y es correcto.
|
||||||
if (!puedeVer) {
|
if (!puedeVer) {
|
||||||
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
|
// Si ya se estableció el error en el fetch de filtros, no lo sobrescribimos.
|
||||||
|
if (!error) setError("No tiene permiso para ver esta sección.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setApiErrorMessage(null);
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
fechaDesde: filtroFechaDesde || null,
|
fechaDesde: filtroFechaDesde || null,
|
||||||
@@ -92,19 +109,27 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
const data = await controlDevolucionesService.getAllControlesDevoluciones(params);
|
const data = await controlDevolucionesService.getAllControlesDevoluciones(params);
|
||||||
setControles(data);
|
setControles(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err); setError('Error al cargar los controles de devoluciones.');
|
console.error(err);
|
||||||
} finally { setLoading(false); }
|
setError('Error al cargar los controles de devoluciones.');
|
||||||
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa]);
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa, error]); // << CAMBIO: Añadido `error` a dependencias
|
||||||
|
|
||||||
useEffect(() => { cargarControles(); }, [cargarControles]);
|
useEffect(() => {
|
||||||
|
// Solo cargar controles si los filtros se han cargado (o intentado cargar)
|
||||||
|
if (!loadingFiltersDropdown) {
|
||||||
|
cargarControles();
|
||||||
|
}
|
||||||
|
}, [cargarControles, loadingFiltersDropdown]); // << CAMBIO: Depende de la carga de filtros
|
||||||
|
|
||||||
|
// ... (resto de los handlers sin cambios) ...
|
||||||
const handleOpenModal = (item?: ControlDevolucionesDto) => {
|
const handleOpenModal = (item?: ControlDevolucionesDto) => {
|
||||||
setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true);
|
setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||||
};
|
};
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setModalOpen(false); setEditingControl(null);
|
setModalOpen(false); setEditingControl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => {
|
const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
try {
|
try {
|
||||||
@@ -119,7 +144,6 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
setApiErrorMessage(message); throw err;
|
setApiErrorMessage(message); throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (idControl: number) => {
|
const handleDelete = async (idControl: number) => {
|
||||||
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
|
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
|
||||||
setApiErrorMessage(null);
|
setApiErrorMessage(null);
|
||||||
@@ -133,26 +157,35 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => {
|
||||||
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
||||||
};
|
};
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setAnchorEl(null); setSelectedRow(null);
|
setAnchorEl(null); setSelectedRow(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0);
|
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||||
};
|
};
|
||||||
// displayData ahora usará la 'controles' directamente, el formato se aplica en el renderizado
|
|
||||||
const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||||
|
|
||||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
// Si no tiene permiso, muestra solo la alerta y nada más.
|
||||||
|
if (!puedeVer) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
{error || "No tiene permiso para acceder a esta sección."}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 1 }}>
|
<Box sx={{ p: 1 }}>
|
||||||
<Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography>
|
<Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography>
|
||||||
|
|
||||||
|
{/* El resto del JSX se renderizará solo si 'puedeVer' es true */}
|
||||||
|
|
||||||
<Paper sx={{ p: 2, mb: 2 }}>
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||||
@@ -169,12 +202,12 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)}
|
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
{(loading || loadingFiltersDropdown) && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||||
|
|
||||||
{!loading && !error && puedeVer && (
|
{!loading && !loadingFiltersDropdown && !error && (
|
||||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */}
|
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 300px)' }}> {/* Ajusta maxHeight */}
|
||||||
<Table stickyHeader size="small">
|
<Table stickyHeader size="small">
|
||||||
<TableHead><TableRow>
|
<TableHead><TableRow>
|
||||||
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
|
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
|
||||||
@@ -186,7 +219,7 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
</TableRow></TableHead>
|
</TableRow></TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{displayData.length === 0 ? (
|
{displayData.length === 0 ? (
|
||||||
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow>
|
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles con los filtros aplicados.</TableCell></TableRow>
|
||||||
) : (
|
) : (
|
||||||
displayData.map((c) => (
|
displayData.map((c) => (
|
||||||
<TableRow key={c.idControl} hover>
|
<TableRow key={c.idControl} hover>
|
||||||
@@ -217,7 +250,10 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
|||||||
{puedeModificar && selectedRow && (
|
{puedeModificar && selectedRow && (
|
||||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||||
{puedeEliminar && selectedRow && (
|
{puedeEliminar && selectedRow && (
|
||||||
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
|
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idControl) }}>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<ControlDevolucionesFormModal
|
<ControlDevolucionesFormModal
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react'; // << Añadido useMemo
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||||
@@ -21,7 +21,7 @@ import canillaService from '../../services/Distribucion/canillaService';
|
|||||||
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
|
||||||
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||||
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto';
|
||||||
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
|
||||||
|
|
||||||
import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
|
import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
|
||||||
@@ -44,8 +44,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
|
|
||||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]);
|
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDropdownDto[]>([]);
|
||||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false)
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
|
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
|
||||||
@@ -81,29 +81,40 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPublicaciones = async () => {
|
const fetchDropdownData = async () => {
|
||||||
|
if (!puedeVer) {
|
||||||
|
setError("No tiene permiso para ver esta sección.");
|
||||||
|
setLoading(false); // Detiene el spinner principal
|
||||||
|
setLoadingFiltersDropdown(false); // Detiene el spinner de los filtros
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||||
setPublicaciones(pubsData);
|
setPublicaciones(pubsData);
|
||||||
|
// La carga de destinatarios se hará en el otro useEffect
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error cargando publicaciones para filtro:",err);
|
console.error("Error cargando publicaciones para filtro:", err);
|
||||||
setError("Error al cargar publicaciones.");
|
setError("Error al cargar publicaciones.");
|
||||||
} finally {
|
} finally {
|
||||||
// No setLoadingFiltersDropdown(false) acá, esperar a la otra carga
|
// La carga finaliza cuando se cargan los destinatarios también.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchPublicaciones();
|
fetchDropdownData();
|
||||||
}, []);
|
}, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia
|
||||||
|
|
||||||
const fetchDestinatariosParaDropdown = useCallback(async () => {
|
const fetchDestinatariosParaDropdown = useCallback(async () => {
|
||||||
|
if (!puedeVer) { return; }
|
||||||
|
|
||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
setFiltroIdCanillitaSeleccionado('');
|
setFiltroIdCanillitaSeleccionado('');
|
||||||
setDestinatariosDropdown([]);
|
setDestinatariosDropdown([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
|
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
|
||||||
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter);
|
const data = await canillaService.getAllDropdownCanillas(true, esAccionistaFilter);
|
||||||
setDestinatariosDropdown(data);
|
setDestinatariosDropdown(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error cargando destinatarios para filtro:", err);
|
console.error("Error cargando destinatarios para filtro:", err);
|
||||||
@@ -111,21 +122,23 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoadingFiltersDropdown(false);
|
setLoadingFiltersDropdown(false);
|
||||||
}
|
}
|
||||||
}, [filtroTipoDestinatario]);
|
}, [filtroTipoDestinatario, puedeVer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDestinatariosParaDropdown();
|
fetchDestinatariosParaDropdown();
|
||||||
}, [fetchDestinatariosParaDropdown]);
|
}, [fetchDestinatariosParaDropdown]);
|
||||||
|
|
||||||
|
|
||||||
const cargarMovimientos = useCallback(async () => {
|
const cargarMovimientos = useCallback(async () => {
|
||||||
if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; }
|
if (!puedeVer) {
|
||||||
|
setError("No tiene permiso para ver esta sección.");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
||||||
if (loading) setLoading(false);
|
if (loading) setLoading(false);
|
||||||
setMovimientos([]);
|
setMovimientos([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
@@ -148,6 +161,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
|
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||||
cargarMovimientos();
|
cargarMovimientos();
|
||||||
@@ -156,8 +170,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
if (loading) setLoading(false);
|
if (loading) setLoading(false);
|
||||||
}
|
}
|
||||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
|
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
|
||||||
|
|
||||||
|
|
||||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||||
if (!puedeCrear && !item) {
|
if (!puedeCrear && !item) {
|
||||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||||
@@ -195,7 +208,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
||||||
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -258,17 +270,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
||||||
setOpenLiquidarDialog(false);
|
setOpenLiquidarDialog(false);
|
||||||
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
||||||
// Necesitamos encontrar el movimiento en la lista ANTES de recargar
|
|
||||||
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
|
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
|
||||||
|
|
||||||
await cargarMovimientos(); // Recargar la lista para reflejar el estado liquidado
|
await cargarMovimientos();
|
||||||
|
|
||||||
// Usar la fecha del movimiento original para el ticket
|
|
||||||
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
||||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||||
await handleImprimirTicketLiquidacion(
|
await handleImprimirTicketLiquidacion(
|
||||||
movimientoParaTicket.idCanilla,
|
movimientoParaTicket.idCanilla,
|
||||||
movimientoParaTicket.fecha, // Usar la fecha del movimiento
|
movimientoParaTicket.fecha,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||||
@@ -328,7 +338,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
} finally { setLoadingTicketPdf(false); }
|
} finally { setLoadingTicketPdf(false); }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||||
@@ -339,8 +348,14 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0)
|
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0)
|
||||||
, [displayData]);
|
, [displayData]);
|
||||||
|
|
||||||
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) {
|
if (!puedeVer) {
|
||||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
return (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
{error || "No tiene permiso para acceder a esta sección."}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
|
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
|
||||||
@@ -352,7 +367,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
|||||||
<Paper sx={{ p: 2, mb: 2 }}>
|
<Paper sx={{ p: 2, mb: 2 }}>
|
||||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||||
{/* ... (Filtros sin cambios) ... */}
|
|
||||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||||
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import distribuidorService from '../../services/Distribucion/distribuidorService
|
|||||||
import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto';
|
import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto';
|
||||||
import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
|
import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
|
||||||
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
|
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
|
||||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||||
|
|
||||||
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
@@ -36,8 +36,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
|||||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||||
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
|
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
|
||||||
|
|
||||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
@@ -69,8 +69,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
|||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
try {
|
try {
|
||||||
const [pubsData, distData] = await Promise.all([
|
const [pubsData, distData] = await Promise.all([
|
||||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
publicacionService.getPublicacionesForDropdown(true),
|
||||||
distribuidorService.getAllDistribuidores()
|
distribuidorService.getAllDistribuidoresDropdown()
|
||||||
]);
|
]);
|
||||||
setPublicaciones(pubsData);
|
setPublicaciones(pubsData);
|
||||||
setDistribuidores(distData);
|
setDistribuidores(distData);
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import otroDestinoService from '../../services/Distribucion/otroDestinoService';
|
|||||||
import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto';
|
import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto';
|
||||||
import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
|
import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
|
||||||
import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
|
import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
|
||||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto';
|
||||||
|
|
||||||
import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal';
|
import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
@@ -34,8 +34,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
|||||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||||
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
||||||
|
|
||||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||||
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
|
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDropdownDto[]>([]);
|
||||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
@@ -68,8 +68,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
|||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
try {
|
try {
|
||||||
const [pubsData, destinosData] = await Promise.all([
|
const [pubsData, destinosData] = await Promise.all([
|
||||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
publicacionService.getPublicacionesForDropdown(true),
|
||||||
otroDestinoService.getAllOtrosDestinos()
|
otroDestinoService.getAllDropdownOtrosDestinos()
|
||||||
]);
|
]);
|
||||||
setPublicaciones(pubsData);
|
setPublicaciones(pubsData);
|
||||||
setOtrosDestinos(destinosData);
|
setOtrosDestinos(destinosData);
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const GestionarEstadosBobinaPage: React.FC = () => {
|
|||||||
|
|
||||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||||
|
|
||||||
// Permisos para Estados de Bobina (ej: IB010 a IB013)
|
|
||||||
const puedeVer = isSuperAdmin || tienePermiso("IB010");
|
const puedeVer = isSuperAdmin || tienePermiso("IB010");
|
||||||
const puedeCrear = isSuperAdmin || tienePermiso("IB011");
|
const puedeCrear = isSuperAdmin || tienePermiso("IB011");
|
||||||
const puedeModificar = isSuperAdmin || tienePermiso("IB012");
|
const puedeModificar = isSuperAdmin || tienePermiso("IB012");
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateSto
|
|||||||
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
|
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
|
||||||
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||||
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
|
||||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
|
||||||
|
|
||||||
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
|
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
|
||||||
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
|
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
|
||||||
@@ -50,8 +50,8 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||||
|
|
||||||
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
|
||||||
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]);
|
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDropdownDto[]>([]);
|
||||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||||
|
|
||||||
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
|
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
|
||||||
@@ -76,9 +76,9 @@ const GestionarStockBobinasPage: React.FC = () => {
|
|||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
try {
|
try {
|
||||||
const [tiposData, plantasData, estadosData] = await Promise.all([
|
const [tiposData, plantasData, estadosData] = await Promise.all([
|
||||||
tipoBobinaService.getAllTiposBobina(),
|
tipoBobinaService.getAllDropdownTiposBobina(),
|
||||||
plantaService.getAllPlantas(),
|
plantaService.getPlantasForDropdown(),
|
||||||
estadoBobinaService.getAllEstadosBobina()
|
estadoBobinaService.getAllDropdownEstadosBobina()
|
||||||
]);
|
]);
|
||||||
setTiposBobina(tiposData);
|
setTiposBobina(tiposData);
|
||||||
setPlantas(plantasData);
|
setPlantas(plantasData);
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import plantaService from '../../services/Impresion/plantaService'; // Para filt
|
|||||||
|
|
||||||
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
|
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
|
||||||
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
|
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
|
||||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
|
||||||
|
|
||||||
import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal';
|
import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
@@ -36,8 +36,8 @@ const GestionarTiradasPage: React.FC = () => {
|
|||||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||||
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
|
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
|
||||||
|
|
||||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
|
||||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
@@ -52,8 +52,8 @@ const GestionarTiradasPage: React.FC = () => {
|
|||||||
setLoadingFiltersDropdown(true);
|
setLoadingFiltersDropdown(true);
|
||||||
try {
|
try {
|
||||||
const [pubsData, plantasData] = await Promise.all([
|
const [pubsData, plantasData] = await Promise.all([
|
||||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
publicacionService.getPublicacionesForDropdown(true),
|
||||||
plantaService.getAllPlantas()
|
plantaService.getPlantasForDropdown()
|
||||||
]);
|
]);
|
||||||
setPublicaciones(pubsData);
|
setPublicaciones(pubsData);
|
||||||
setPlantas(plantasData);
|
setPlantas(plantasData);
|
||||||
|
|||||||
@@ -8,67 +8,68 @@ import SaveIcon from '@mui/icons-material/Save';
|
|||||||
import perfilService from '../../services/Usuarios/perfilService';
|
import perfilService from '../../services/Usuarios/perfilService';
|
||||||
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||||
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; // Renombrar para evitar conflicto
|
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist';
|
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist';
|
||||||
|
|
||||||
const SECCION_PERMISSIONS_PREFIX = "SS";
|
const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||||
|
|
||||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||||
if (codAcc === "SS001") return "Distribución";
|
if (codAcc === "SS001") return "Distribución";
|
||||||
if (codAcc === "SS002") return "Contables";
|
if (codAcc === "SS002") return "Contables";
|
||||||
if (codAcc === "SS003") return "Impresión";
|
if (codAcc === "SS003") return "Impresión";
|
||||||
if (codAcc === "SS004") return "Reportes";
|
if (codAcc === "SS004") return "Reportes";
|
||||||
if (codAcc === "SS005") return "Radios";
|
if (codAcc === "SS005") return "Radios";
|
||||||
if (codAcc === "SS006") return "Usuarios";
|
if (codAcc === "SS006") return "Usuarios";
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||||
const moduloLower = permisoModulo.toLowerCase();
|
const moduloLower = permisoModulo.toLowerCase();
|
||||||
if (moduloLower.includes("distribuidores") ||
|
if (moduloLower.includes("distribuidores") ||
|
||||||
moduloLower.includes("canillas") ||
|
moduloLower.includes("canillas") ||
|
||||||
moduloLower.includes("publicaciones distribución") ||
|
moduloLower.includes("publicaciones distribución") ||
|
||||||
moduloLower.includes("zonas distribuidores") ||
|
moduloLower.includes("zonas distribuidores") ||
|
||||||
moduloLower.includes("movimientos distribuidores") ||
|
moduloLower.includes("movimientos distribuidores") ||
|
||||||
moduloLower.includes("empresas") ||
|
moduloLower.includes("empresas") ||
|
||||||
moduloLower.includes("otros destinos") ||
|
moduloLower.includes("otros destinos") ||
|
||||||
moduloLower.includes("ctrl. devoluciones") ||
|
moduloLower.includes("ctrl. devoluciones") ||
|
||||||
moduloLower.includes("movimientos canillas") ||
|
moduloLower.includes("movimientos canillas") ||
|
||||||
moduloLower.includes("salidas otros destinos")) {
|
moduloLower.includes("salidas otros destinos")) {
|
||||||
return "Distribución";
|
return "Distribución";
|
||||||
}
|
}
|
||||||
if (moduloLower.includes("cuentas pagos") ||
|
if (moduloLower.includes("cuentas pagos") ||
|
||||||
moduloLower.includes("cuentas notas") ||
|
moduloLower.includes("cuentas notas") ||
|
||||||
moduloLower.includes("cuentas tipos pagos")) {
|
moduloLower.includes("cuentas tipos pagos")) {
|
||||||
return "Contables";
|
return "Contables";
|
||||||
}
|
}
|
||||||
if (moduloLower.includes("impresión tiradas") ||
|
if (moduloLower.includes("impresión tiradas") ||
|
||||||
moduloLower.includes("impresión bobinas") ||
|
moduloLower.includes("impresión bobinas") ||
|
||||||
moduloLower.includes("impresión plantas") ||
|
moduloLower.includes("impresión plantas") ||
|
||||||
moduloLower.includes("tipos bobinas")) {
|
moduloLower.includes("estados bobinas") ||
|
||||||
return "Impresión";
|
moduloLower.includes("tipos bobinas")) {
|
||||||
}
|
return "Impresión";
|
||||||
if (moduloLower.includes("radios")) {
|
}
|
||||||
return "Radios";
|
if (moduloLower.includes("radios")) {
|
||||||
}
|
return "Radios";
|
||||||
if (moduloLower.includes("usuarios") ||
|
}
|
||||||
moduloLower.includes("perfiles")) {
|
if (moduloLower.includes("usuarios") ||
|
||||||
return "Usuarios";
|
moduloLower.includes("perfiles")) {
|
||||||
}
|
return "Usuarios";
|
||||||
if (moduloLower.includes("reportes")) {
|
}
|
||||||
return "Reportes";
|
if (moduloLower.includes("reportes")) {
|
||||||
}
|
return "Reportes";
|
||||||
if (moduloLower.includes("permisos")) {
|
}
|
||||||
return "Permisos (Definición)";
|
if (moduloLower.includes("permisos")) {
|
||||||
}
|
return "Permisos (Definición)";
|
||||||
return permisoModulo;
|
}
|
||||||
|
return permisoModulo;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AsignarPermisosAPerfilPage: React.FC = () => {
|
const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||||
const { idPerfil } = useParams<{ idPerfil: string }>();
|
const { idPerfil } = useParams<{ idPerfil: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); // Renombrado
|
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions();
|
||||||
|
|
||||||
const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004");
|
const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004");
|
||||||
|
|
||||||
@@ -124,76 +125,75 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
|||||||
moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo
|
moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo
|
||||||
) => {
|
) => {
|
||||||
setPermisosSeleccionados(prevSelected => {
|
setPermisosSeleccionados(prevSelected => {
|
||||||
const newSelected = new Set(prevSelected);
|
const newSelected = new Set(prevSelected);
|
||||||
const permisoActual = permisosDisponibles.find(p => p.id === permisoId);
|
const permisoActual = permisosDisponibles.find(p => p.id === permisoId);
|
||||||
if (!permisoActual) return prevSelected;
|
if (!permisoActual) return prevSelected;
|
||||||
|
|
||||||
const permisosDelModuloHijo = moduloConceptualAsociado
|
const permisosDelModuloHijo = moduloConceptualAsociado
|
||||||
? permisosDisponibles.filter(p => {
|
? permisosDisponibles.filter(p => {
|
||||||
const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper
|
const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper
|
||||||
return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX);
|
return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX);
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (esPermisoSeccionClick && moduloConceptualAsociado) {
|
if (esPermisoSeccionClick && moduloConceptualAsociado) {
|
||||||
const idPermisoSeccion = permisoActual.id;
|
const idPermisoSeccion = permisoActual.id;
|
||||||
const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion);
|
const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion);
|
||||||
const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id));
|
const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id));
|
||||||
const ningunHijoEstabaSeleccionado = 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)
|
if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos)
|
||||||
newSelected.add(idPermisoSeccion);
|
newSelected.add(idPermisoSeccion);
|
||||||
// NO se marcan los hijos
|
// NO se marcan los hijos
|
||||||
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0 ) {
|
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0) {
|
||||||
// Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos"
|
// Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos"
|
||||||
newSelected.add(idPermisoSeccion); // Asegurar
|
newSelected.add(idPermisoSeccion); // Asegurar
|
||||||
permisosDelModuloHijo.forEach(p => newSelected.add(p.id));
|
permisosDelModuloHijo.forEach(p => newSelected.add(p.id));
|
||||||
} else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off
|
} else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off
|
||||||
newSelected.delete(idPermisoSeccion);
|
newSelected.delete(idPermisoSeccion);
|
||||||
permisosDelModuloHijo.forEach(p => newSelected.delete(p.id));
|
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);
|
} else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo
|
||||||
if (error) setError(null);
|
if (asignadoViaCheckboxHijo) {
|
||||||
return newSelected;
|
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;
|
||||||
});
|
});
|
||||||
}, [permisosDisponibles, successMessage, error]);
|
}, [permisosDisponibles, successMessage, error]);
|
||||||
|
|
||||||
|
|
||||||
const handleGuardarCambios = async () => {
|
const handleGuardarCambios = async () => {
|
||||||
// ... (sin cambios) ...
|
|
||||||
if (!puedeAsignar || !perfil) return;
|
if (!puedeAsignar || !perfil) return;
|
||||||
setSaving(true); setError(null); setSuccessMessage(null);
|
setSaving(true); setError(null); setSuccessMessage(null);
|
||||||
try {
|
try {
|
||||||
@@ -214,54 +214,54 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
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) {
|
if (error && !perfil) {
|
||||||
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
|
||||||
}
|
}
|
||||||
if (!puedeAsignar) {
|
if (!puedeAsignar) {
|
||||||
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
|
||||||
}
|
}
|
||||||
if (!perfil && !loading) {
|
if (!perfil && !loading) {
|
||||||
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>;
|
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 1 }}>
|
<Box sx={{ p: 1 }}>
|
||||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
|
||||||
Volver a Perfiles
|
Volver a Perfiles
|
||||||
</Button>
|
</Button>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||||
ID Perfil: {perfil?.id}
|
ID Perfil: {perfil?.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
{error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||||
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
|
||||||
|
|
||||||
<Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}>
|
<Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}>
|
||||||
<PermisosChecklist
|
<PermisosChecklist
|
||||||
permisosDisponibles={permisosDisponibles}
|
permisosDisponibles={permisosDisponibles}
|
||||||
permisosSeleccionados={permisosSeleccionados}
|
permisosSeleccionados={permisosSeleccionados}
|
||||||
onPermisoChange={handlePermisoChange}
|
onPermisoChange={handlePermisoChange}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
|
||||||
onClick={handleGuardarCambios}
|
onClick={handleGuardarCambios}
|
||||||
disabled={saving || !puedeAsignar}
|
disabled={saving || !puedeAsignar}
|
||||||
>
|
>
|
||||||
Guardar Cambios
|
Guardar Cambios
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default AsignarPermisosAPerfilPage;
|
export default AsignarPermisosAPerfilPage;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
import { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
|
||||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
|
|
||||||
const usuariosSubModules = [
|
const usuariosSubModules = [
|
||||||
{ label: 'Perfiles', path: 'perfiles' },
|
{ label: 'Perfiles', path: 'perfiles' },
|
||||||
@@ -13,37 +14,54 @@ const UsuariosIndexPage: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||||
|
const { isSuperAdmin } = usePermissions();
|
||||||
|
|
||||||
|
// --- Filtrar solo lo que puede ver este usuario ---
|
||||||
|
const availableSubModules = useMemo(
|
||||||
|
() =>
|
||||||
|
usuariosSubModules.filter(sub => {
|
||||||
|
// Estos dos ítems solo para superadmins
|
||||||
|
if (
|
||||||
|
(sub.path === 'permisos' || sub.path === 'auditoria-usuarios')
|
||||||
|
&& !isSuperAdmin
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
[isSuperAdmin]
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Ajustar la pestaña activa según la ruta ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentBasePath = '/usuarios';
|
const base = '/usuarios';
|
||||||
const subPath = location.pathname.startsWith(currentBasePath + '/')
|
let subPath: string | undefined;
|
||||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta
|
if (location.pathname.startsWith(base + '/')) {
|
||||||
: (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined);
|
subPath = location.pathname.slice(base.length + 1).split('/')[0];
|
||||||
|
} else if (location.pathname === base) {
|
||||||
const activeTabIndex = usuariosSubModules.findIndex(
|
subPath = availableSubModules[0]?.path;
|
||||||
(subModule) => subModule.path === subPath
|
|
||||||
);
|
|
||||||
|
|
||||||
if (activeTabIndex !== -1) {
|
|
||||||
setSelectedSubTab(activeTabIndex);
|
|
||||||
} else {
|
|
||||||
if (location.pathname === currentBasePath && usuariosSubModules.length > 0) {
|
|
||||||
navigate(usuariosSubModules[0].path, { replace: true });
|
|
||||||
setSelectedSubTab(0);
|
|
||||||
} else {
|
|
||||||
setSelectedSubTab(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [location.pathname, navigate]);
|
const idx = availableSubModules.findIndex(m => m.path === subPath);
|
||||||
|
if (idx !== -1) {
|
||||||
|
setSelectedSubTab(idx);
|
||||||
|
} else if (location.pathname === base && availableSubModules.length) {
|
||||||
|
navigate(availableSubModules[0].path, { replace: true });
|
||||||
|
setSelectedSubTab(0);
|
||||||
|
} else {
|
||||||
|
setSelectedSubTab(false);
|
||||||
|
}
|
||||||
|
}, [location.pathname, navigate, availableSubModules]);
|
||||||
|
|
||||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
const handleSubTabChange = (_: any, newValue: number) => {
|
||||||
setSelectedSubTab(newValue);
|
setSelectedSubTab(newValue);
|
||||||
navigate(usuariosSubModules[newValue].path);
|
navigate(availableSubModules[newValue].path);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h5" gutterBottom>Módulo de Usuarios y Seguridad</Typography>
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Módulo de Usuarios y Seguridad
|
||||||
|
</Typography>
|
||||||
<Paper square elevation={1}>
|
<Paper square elevation={1}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={selectedSubTab}
|
value={selectedSubTab}
|
||||||
@@ -54,8 +72,8 @@ const UsuariosIndexPage: React.FC = () => {
|
|||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
aria-label="sub-módulos de usuarios"
|
aria-label="sub-módulos de usuarios"
|
||||||
>
|
>
|
||||||
{usuariosSubModules.map((subModule) => (
|
{availableSubModules.map(sub => (
|
||||||
<Tab key={subModule.path} label={subModule.label} />
|
<Tab key={sub.path} label={sub.label} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -66,4 +84,4 @@ const UsuariosIndexPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UsuariosIndexPage;
|
export default UsuariosIndexPage;
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ import GestionarNovedadesCanillaPage from '../pages/Distribucion/GestionarNoveda
|
|||||||
import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage';
|
import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage';
|
||||||
import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage';
|
import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage';
|
||||||
|
|
||||||
|
// Anonalías
|
||||||
|
import AlertasPage from '../pages/Anomalia/AlertasPage';
|
||||||
|
|
||||||
// Auditorias
|
// Auditorias
|
||||||
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
||||||
import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage';
|
import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage';
|
||||||
@@ -130,6 +133,19 @@ const AppRoutes = () => {
|
|||||||
{/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */}
|
{/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */}
|
||||||
<Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */}
|
<Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */}
|
||||||
|
|
||||||
|
{/* Módulo de Anomalías */}
|
||||||
|
<Route
|
||||||
|
path="anomalias"
|
||||||
|
element={
|
||||||
|
<SectionProtectedRoute requiredPermission="AL001" sectionName="Anomalías">
|
||||||
|
<Outlet />
|
||||||
|
</SectionProtectedRoute>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route index element={<Navigate to="alertas" replace />} />
|
||||||
|
<Route path="alertas" element={<AlertasPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Módulo de Distribución (anidado) */}
|
{/* Módulo de Distribución (anidado) */}
|
||||||
<Route
|
<Route
|
||||||
path="distribucion"
|
path="distribucion"
|
||||||
|
|||||||
49
Frontend/src/services/Anomalia/alertaService.ts
Normal file
49
Frontend/src/services/Anomalia/alertaService.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import apiClient from '../apiClient';
|
||||||
|
|
||||||
|
// El contrato que define la estructura de una alerta genérica
|
||||||
|
export interface AlertaGenericaDto {
|
||||||
|
idAlerta: number;
|
||||||
|
fechaDeteccion: string;
|
||||||
|
tipoAlerta: string;
|
||||||
|
entidad: string;
|
||||||
|
idEntidad: number;
|
||||||
|
mensaje: string;
|
||||||
|
fechaAnomalia: string;
|
||||||
|
leida: boolean;
|
||||||
|
cantidadEnviada?: number;
|
||||||
|
cantidadDevuelta?: number;
|
||||||
|
porcentajeDevolucion?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO para el request de marcar un grupo como leído
|
||||||
|
export interface MarcarGrupoLeidoRequestDto {
|
||||||
|
tipoAlerta: string;
|
||||||
|
idEntidad: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene todas las alertas no leídas del sistema.
|
||||||
|
*/
|
||||||
|
export const getAlertas = async (): Promise<AlertaGenericaDto[]> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<AlertaGenericaDto[]>('/alertas');
|
||||||
|
return response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error en getAlertas:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marca una única alerta como leída.
|
||||||
|
*/
|
||||||
|
export const marcarAlertaLeida = async (idAlerta: number): Promise<void> => {
|
||||||
|
await apiClient.post(`/alertas/${idAlerta}/marcar-leida`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marca un grupo completo de alertas como leídas.
|
||||||
|
*/
|
||||||
|
export const marcarGrupoComoLeido = async (request: MarcarGrupoLeidoRequestDto): Promise<void> => {
|
||||||
|
await apiClient.post('/alertas/marcar-grupo-leido', request);
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
|
|||||||
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
|
||||||
import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto';
|
import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto';
|
||||||
import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto';
|
import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto';
|
||||||
|
import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto';
|
||||||
|
|
||||||
|
|
||||||
const getAllCanillas = async (
|
const getAllCanillas = async (
|
||||||
@@ -15,12 +16,24 @@ const getAllCanillas = async (
|
|||||||
if (nomApeFilter) params.nomApe = nomApeFilter;
|
if (nomApeFilter) params.nomApe = nomApeFilter;
|
||||||
if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter;
|
if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter;
|
||||||
if (soloActivos !== undefined) params.soloActivos = soloActivos;
|
if (soloActivos !== undefined) params.soloActivos = soloActivos;
|
||||||
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; // <<-- ¡CLAVE! Verifica esto.
|
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter;
|
||||||
|
|
||||||
const response = await apiClient.get<CanillaDto[]>('/canillas', { params });
|
const response = await apiClient.get<CanillaDto[]>('/canillas', { params });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllDropdownCanillas = async (
|
||||||
|
soloActivos?: boolean,
|
||||||
|
esAccionistaFilter?: boolean // Asegúrate que esté aquí
|
||||||
|
): Promise<CanillaDropdownDto[]> => {
|
||||||
|
const params: Record<string, string | number | boolean> = {};
|
||||||
|
if (soloActivos !== undefined) params.soloActivos = soloActivos;
|
||||||
|
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter;
|
||||||
|
|
||||||
|
const response = await apiClient.get<CanillaDropdownDto[]>('/canillas/dropdown', { params });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
const getCanillaById = async (id: number): Promise<CanillaDto> => {
|
const getCanillaById = async (id: number): Promise<CanillaDto> => {
|
||||||
const response = await apiClient.get<CanillaDto>(`/canillas/${id}`);
|
const response = await apiClient.get<CanillaDto>(`/canillas/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -43,6 +56,7 @@ const toggleBajaCanilla = async (id: number, data: ToggleBajaCanillaDto): Promis
|
|||||||
|
|
||||||
const canillaService = {
|
const canillaService = {
|
||||||
getAllCanillas,
|
getAllCanillas,
|
||||||
|
getAllDropdownCanillas,
|
||||||
getCanillaById,
|
getCanillaById,
|
||||||
createCanilla,
|
createCanilla,
|
||||||
updateCanilla,
|
updateCanilla,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
|
|||||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||||
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
||||||
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
|
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
|
||||||
|
import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto';
|
||||||
|
|
||||||
const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => {
|
const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
@@ -12,6 +13,12 @@ const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDt
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllDropdownOtrosDestinos = async (): Promise<OtroDestinoDto[]> => {
|
||||||
|
// Llama a GET /api/otrosdestinos/dropdown
|
||||||
|
const response = await apiClient.get<OtroDestinoDropdownDto[]>('/otrosdestinos/dropdown');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
const getOtroDestinoById = async (id: number): Promise<OtroDestinoDto> => {
|
const getOtroDestinoById = async (id: number): Promise<OtroDestinoDto> => {
|
||||||
// Llama a GET /api/otrosdestinos/{id}
|
// Llama a GET /api/otrosdestinos/{id}
|
||||||
const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`);
|
const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`);
|
||||||
@@ -36,6 +43,7 @@ const deleteOtroDestino = async (id: number): Promise<void> => {
|
|||||||
|
|
||||||
const otroDestinoService = {
|
const otroDestinoService = {
|
||||||
getAllOtrosDestinos,
|
getAllOtrosDestinos,
|
||||||
|
getAllDropdownOtrosDestinos,
|
||||||
getOtroDestinoById,
|
getOtroDestinoById,
|
||||||
createOtroDestino,
|
createOtroDestino,
|
||||||
updateOtroDestino,
|
updateOtroDestino,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
|
|||||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
||||||
import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto';
|
import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto';
|
||||||
import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto';
|
import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto';
|
||||||
|
import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
|
||||||
|
|
||||||
const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => {
|
const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
@@ -11,6 +12,11 @@ const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoB
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllDropdownEstadosBobina = async (): Promise<EstadoBobinaDropdownDto[]> => {
|
||||||
|
const response = await apiClient.get<EstadoBobinaDropdownDto[]>('/estadosbobina/dropdown');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
const getEstadoBobinaById = async (id: number): Promise<EstadoBobinaDto> => {
|
const getEstadoBobinaById = async (id: number): Promise<EstadoBobinaDto> => {
|
||||||
const response = await apiClient.get<EstadoBobinaDto>(`/estadosbobina/${id}`);
|
const response = await apiClient.get<EstadoBobinaDto>(`/estadosbobina/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -31,6 +37,7 @@ const deleteEstadoBobina = async (id: number): Promise<void> => {
|
|||||||
|
|
||||||
const estadoBobinaService = {
|
const estadoBobinaService = {
|
||||||
getAllEstadosBobina,
|
getAllEstadosBobina,
|
||||||
|
getAllDropdownEstadosBobina,
|
||||||
getEstadoBobinaById,
|
getEstadoBobinaById,
|
||||||
createEstadoBobina,
|
createEstadoBobina,
|
||||||
updateEstadoBobina,
|
updateEstadoBobina,
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ const getAllTiposBobina = async (denominacionFilter?: string): Promise<TipoBobin
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllDropdownTiposBobina = async (): Promise<TipoBobinaDto[]> => {
|
||||||
|
// Llama a GET /api/tiposbobina/dropdown
|
||||||
|
const response = await apiClient.get<TipoBobinaDto[]>('/tiposbobina/dropdown');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
const getTipoBobinaById = async (id: number): Promise<TipoBobinaDto> => {
|
const getTipoBobinaById = async (id: number): Promise<TipoBobinaDto> => {
|
||||||
// Llama a GET /api/tiposbobina/{id}
|
// Llama a GET /api/tiposbobina/{id}
|
||||||
const response = await apiClient.get<TipoBobinaDto>(`/tiposbobina/${id}`);
|
const response = await apiClient.get<TipoBobinaDto>(`/tiposbobina/${id}`);
|
||||||
@@ -36,6 +42,7 @@ const deleteTipoBobina = async (id: number): Promise<void> => {
|
|||||||
|
|
||||||
const tipoBobinaService = {
|
const tipoBobinaService = {
|
||||||
getAllTiposBobina,
|
getAllTiposBobina,
|
||||||
|
getAllDropdownTiposBobina,
|
||||||
getTipoBobinaById,
|
getTipoBobinaById,
|
||||||
createTipoBobina,
|
createTipoBobina,
|
||||||
updateTipoBobina,
|
updateTipoBobina,
|
||||||
|
|||||||
134
ProyectoIA_Gestion/detect.py
Normal file
134
ProyectoIA_Gestion/detect.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import joblib
|
||||||
|
import os
|
||||||
|
import pyodbc
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def insertar_alerta_en_db(cursor, tipo_alerta, id_entidad, entidad, mensaje, fecha_anomalia, cant_enviada=None, cant_devuelta=None, porc_devolucion=None):
|
||||||
|
"""Función centralizada para insertar en la nueva tabla Sistema_Alertas."""
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO Sistema_Alertas
|
||||||
|
(TipoAlerta, IdEntidad, Entidad, Mensaje, FechaAnomalia, CantidadEnviada, CantidadDevuelta, PorcentajeDevolucion, Leida)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Asegurarse de que los valores numéricos opcionales sean None si no se proporcionan
|
||||||
|
p_dev = float(porc_devolucion) if porc_devolucion is not None else None
|
||||||
|
c_env = int(cant_enviada) if cant_enviada is not None else None
|
||||||
|
c_dev = int(cant_devuelta) if cant_devuelta is not None else None
|
||||||
|
|
||||||
|
cursor.execute(insert_query, tipo_alerta, id_entidad, entidad, mensaje, fecha_anomalia, c_env, c_dev, p_dev)
|
||||||
|
print(f"INFO: Alerta '{tipo_alerta}' para '{entidad}' ID {id_entidad} registrada.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: No se pudo insertar la alerta para '{entidad}' ID {id_entidad}. Error: {e}")
|
||||||
|
|
||||||
|
print("--- INICIANDO SCRIPT DE DETECCIÓN COMPLETO ---")
|
||||||
|
|
||||||
|
# --- 1. Configuración ---
|
||||||
|
DB_SERVER = 'TECNICA3'
|
||||||
|
DB_DATABASE = 'SistemaGestion'
|
||||||
|
CONNECTION_STRING = f'DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes;TrustServerCertificate=yes;'
|
||||||
|
MODEL_INDIVIDUAL_FILE = 'modelo_anomalias.joblib'
|
||||||
|
MODEL_SISTEMA_FILE = 'modelo_sistema_anomalias.joblib'
|
||||||
|
|
||||||
|
# --- 2. Determinar Fecha ---
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
target_date = datetime.strptime(sys.argv[1], '%Y-%m-%d')
|
||||||
|
else:
|
||||||
|
target_date = datetime.now() - timedelta(days=1)
|
||||||
|
print(f"--- FECHA DE ANÁLISIS: {target_date.date()} ---")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cnxn = pyodbc.connect(CONNECTION_STRING)
|
||||||
|
cursor = cnxn.cursor()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"CRITICAL: No se pudo conectar a la base de datos. Error: {e}")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# --- 3. DETECCIÓN INDIVIDUAL (CANILLITAS) ---
|
||||||
|
print("\n--- FASE 1: Detección de Anomalías Individuales (Canillitas) ---")
|
||||||
|
if not os.path.exists(MODEL_INDIVIDUAL_FILE):
|
||||||
|
print(f"ADVERTENCIA: Modelo individual '{MODEL_INDIVIDUAL_FILE}' no encontrado.")
|
||||||
|
else:
|
||||||
|
model_individual = joblib.load(MODEL_INDIVIDUAL_FILE)
|
||||||
|
query_individual = f"""
|
||||||
|
SELECT esc.Id_Canilla AS id_canilla, esc.Fecha AS fecha, esc.CantSalida AS cantidad_enviada, esc.CantEntrada AS cantidad_devuelta, c.NomApe AS nombre_canilla
|
||||||
|
FROM dist_EntradasSalidasCanillas esc
|
||||||
|
JOIN dist_dtCanillas c ON esc.Id_Canilla = c.Id_Canilla
|
||||||
|
WHERE CAST(Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' AND CantSalida > 0
|
||||||
|
"""
|
||||||
|
df_new = pd.read_sql(query_individual, cnxn)
|
||||||
|
|
||||||
|
if not df_new.empty:
|
||||||
|
df_new['porcentaje_devolucion'] = (df_new['cantidad_devuelta'] / df_new['cantidad_enviada']).fillna(0) * 100
|
||||||
|
df_new['dia_semana'] = pd.to_datetime(df_new['fecha']).dt.dayofweek
|
||||||
|
features = ['id_canilla', 'porcentaje_devolucion', 'dia_semana']
|
||||||
|
X_new = df_new[features]
|
||||||
|
df_new['anomalia'] = model_individual.predict(X_new)
|
||||||
|
anomalias_detectadas = df_new[df_new['anomalia'] == -1]
|
||||||
|
|
||||||
|
if not anomalias_detectadas.empty:
|
||||||
|
for index, row in anomalias_detectadas.iterrows():
|
||||||
|
mensaje = f"Devolución del {row['porcentaje_devolucion']:.2f}% para '{row['nombre_canilla']}'."
|
||||||
|
insertar_alerta_en_db(cursor,
|
||||||
|
tipo_alerta='DevolucionAnomala',
|
||||||
|
id_entidad=row['id_canilla'],
|
||||||
|
entidad='Canillita',
|
||||||
|
mensaje=mensaje,
|
||||||
|
fecha_anomalia=row['fecha'].date(),
|
||||||
|
cant_enviada=row['cantidad_enviada'],
|
||||||
|
cant_devuelta=row['cantidad_devuelta'],
|
||||||
|
porc_devolucion=row['porcentaje_devolucion'])
|
||||||
|
else:
|
||||||
|
print("INFO: No se encontraron anomalías individuales significativas.")
|
||||||
|
else:
|
||||||
|
print("INFO: No hay datos de canillitas para analizar en la fecha seleccionada.")
|
||||||
|
|
||||||
|
# --- 4. DETECCIÓN DE SISTEMA ---
|
||||||
|
print("\n--- FASE 2: Detección de Anomalías de Sistema ---")
|
||||||
|
if not os.path.exists(MODEL_SISTEMA_FILE):
|
||||||
|
print(f"ADVERTENCIA: Modelo de sistema '{MODEL_SISTEMA_FILE}' no encontrado.")
|
||||||
|
else:
|
||||||
|
model_sistema = joblib.load(MODEL_SISTEMA_FILE)
|
||||||
|
query_agregada = f"""
|
||||||
|
SELECT CAST(Fecha AS DATE) AS fecha_dia, DATEPART(weekday, Fecha) as dia_semana,
|
||||||
|
COUNT(DISTINCT Id_Canilla) as total_canillitas_activos,
|
||||||
|
SUM(CantSalida) as total_salidas, SUM(CantEntrada) as total_devoluciones
|
||||||
|
FROM dist_EntradasSalidasCanillas
|
||||||
|
WHERE CAST(Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' AND CantSalida > 0
|
||||||
|
GROUP BY CAST(Fecha AS DATE), DATEPART(weekday, Fecha)
|
||||||
|
"""
|
||||||
|
df_system = pd.read_sql(query_agregada, cnxn)
|
||||||
|
|
||||||
|
if not df_system.empty and df_system['total_salidas'].iloc[0] > 0:
|
||||||
|
df_system['ratio_devolucion'] = (df_system['total_devoluciones'] / df_system['total_salidas']).fillna(0)
|
||||||
|
df_system['salidas_por_canillita'] = (df_system['total_salidas'] / df_system['total_canillitas_activos']).fillna(0)
|
||||||
|
features_system = ['dia_semana', 'total_salidas', 'ratio_devolucion', 'salidas_por_canillita']
|
||||||
|
X_system = df_system[features_system]
|
||||||
|
df_system['anomalia_sistema'] = model_sistema.predict(X_system)
|
||||||
|
|
||||||
|
if df_system['anomalia_sistema'].iloc[0] == -1:
|
||||||
|
ratio_hoy = df_system['ratio_devolucion'].iloc[0] * 100
|
||||||
|
mensaje = f"El ratio de devolución global fue del {ratio_hoy:.2f}%, un valor atípico para este día de la semana."
|
||||||
|
insertar_alerta_en_db(cursor,
|
||||||
|
tipo_alerta='ComportamientoSistema',
|
||||||
|
id_entidad=0,
|
||||||
|
entidad='Sistema',
|
||||||
|
mensaje=mensaje,
|
||||||
|
fecha_anomalia=target_date.date())
|
||||||
|
else:
|
||||||
|
print("INFO: El comportamiento agregado del sistema fue normal.")
|
||||||
|
else:
|
||||||
|
mensaje = f"ALERTA GRAVE: No se registraron movimientos de salida para ningún canillita en la fecha {target_date.date()}."
|
||||||
|
insertar_alerta_en_db(cursor,
|
||||||
|
tipo_alerta='FaltaDeDatos',
|
||||||
|
id_entidad=0,
|
||||||
|
entidad='Sistema',
|
||||||
|
mensaje=mensaje,
|
||||||
|
fecha_anomalia=target_date.date())
|
||||||
|
|
||||||
|
# --- 5. Finalización ---
|
||||||
|
cnxn.commit()
|
||||||
|
cnxn.close()
|
||||||
|
print("\n--- DETECCIÓN COMPLETA ---")
|
||||||
BIN
ProyectoIA_Gestion/modelo_anomalias.joblib
Normal file
BIN
ProyectoIA_Gestion/modelo_anomalias.joblib
Normal file
Binary file not shown.
BIN
ProyectoIA_Gestion/modelo_sistema_anomalias.joblib
Normal file
BIN
ProyectoIA_Gestion/modelo_sistema_anomalias.joblib
Normal file
Binary file not shown.
65
ProyectoIA_Gestion/train.py
Normal file
65
ProyectoIA_Gestion/train.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from sklearn.ensemble import IsolationForest
|
||||||
|
import joblib
|
||||||
|
import os
|
||||||
|
import pyodbc
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
print("--- INICIANDO SCRIPT DE ENTRENAMIENTO (CONEXIÓN BD) ---")
|
||||||
|
|
||||||
|
# --- 1. Configuración de Conexión y Parámetros ---
|
||||||
|
DB_SERVER = 'TECNICA3' # O el nombre de tu instancia, ej: '.\SQLEXPRESS'
|
||||||
|
DB_DATABASE = 'SistemaGestion' # El nombre de tu base de datos
|
||||||
|
# Para autenticación de Windows, usa la siguiente línea:
|
||||||
|
CONNECTION_STRING = f'DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes;TrustServerCertificate=yes;'
|
||||||
|
|
||||||
|
MODEL_FILE = 'modelo_anomalias.joblib'
|
||||||
|
CONTAMINATION_RATE = 0.001 # Tasa de contaminación del 0.013% (ajustable según tus necesidades)
|
||||||
|
|
||||||
|
# --- 2. Carga de Datos desde SQL Server ---
|
||||||
|
try:
|
||||||
|
print(f"Conectando a la base de datos '{DB_DATABASE}' en '{DB_SERVER}'...")
|
||||||
|
cnxn = pyodbc.connect(CONNECTION_STRING)
|
||||||
|
|
||||||
|
# Tomamos el último año de datos para el entrenamiento
|
||||||
|
fecha_limite = datetime.now() - timedelta(days=365)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
Id_Canilla AS id_canilla,
|
||||||
|
Fecha AS fecha,
|
||||||
|
CantSalida AS cantidad_enviada,
|
||||||
|
CantEntrada AS cantidad_devuelta
|
||||||
|
FROM
|
||||||
|
dist_EntradasSalidasCanillas
|
||||||
|
WHERE
|
||||||
|
Fecha >= '{fecha_limite.strftime('%Y-%m-%d')}'
|
||||||
|
"""
|
||||||
|
print("Ejecutando consulta para obtener datos de entrenamiento...")
|
||||||
|
df = pd.read_sql(query, cnxn)
|
||||||
|
cnxn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al conectar o consultar la base de datos: {e}")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print("No se encontraron datos de entrenamiento en el último año. Saliendo.")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# --- 3. Preparación de Datos (sin cambios) ---
|
||||||
|
print(f"Preparando {len(df)} registros para el entrenamiento...")
|
||||||
|
df['porcentaje_devolucion'] = (df['cantidad_devuelta'] / (df['cantidad_enviada'] + 0.001)) * 100
|
||||||
|
df.fillna(0, inplace=True)
|
||||||
|
df['porcentaje_devolucion'] = df['porcentaje_devolucion'].clip(0, 100)
|
||||||
|
df['dia_semana'] = df['fecha'].dt.dayofweek
|
||||||
|
features = ['id_canilla', 'porcentaje_devolucion', 'dia_semana']
|
||||||
|
X = df[features]
|
||||||
|
|
||||||
|
# --- 4. Entrenamiento y Guardado (sin cambios) ---
|
||||||
|
print(f"Entrenando el modelo con tasa de contaminación de {CONTAMINATION_RATE}...")
|
||||||
|
model = IsolationForest(n_estimators=100, contamination=CONTAMINATION_RATE, random_state=42)
|
||||||
|
model.fit(X)
|
||||||
|
joblib.dump(model, MODEL_FILE)
|
||||||
|
|
||||||
|
print(f"--- ENTRENAMIENTO COMPLETADO. Modelo guardado en '{MODEL_FILE}' ---")
|
||||||
71
ProyectoIA_Gestion/train_sistema.py
Normal file
71
ProyectoIA_Gestion/train_sistema.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from sklearn.ensemble import IsolationForest
|
||||||
|
import joblib
|
||||||
|
import os
|
||||||
|
import pyodbc
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
print("--- INICIANDO SCRIPT DE ENTRENAMIENTO DE SISTEMA ---")
|
||||||
|
|
||||||
|
# --- 1. Configuración ---
|
||||||
|
DB_SERVER = 'TECNICA3'
|
||||||
|
DB_DATABASE = 'SistemaGestion'
|
||||||
|
CONNECTION_STRING = f'DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes;TrustServerCertificate=yes;'
|
||||||
|
MODEL_FILE = 'modelo_sistema_anomalias.joblib'
|
||||||
|
CONTAMINATION_RATE = 0.02 # Tasa de contaminación del 0.2% (ajustable según tus necesidades)
|
||||||
|
|
||||||
|
# --- 2. Carga y Agregación de Datos desde SQL Server ---
|
||||||
|
try:
|
||||||
|
print("Conectando a la base de datos...")
|
||||||
|
cnxn = pyodbc.connect(CONNECTION_STRING)
|
||||||
|
|
||||||
|
# Consulta para agregar los datos por día
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
CAST(Fecha AS DATE) AS fecha_dia,
|
||||||
|
DATEPART(weekday, Fecha) as dia_semana, -- 1=Domingo, 2=Lunes...
|
||||||
|
COUNT(DISTINCT Id_Canilla) as total_canillitas_activos,
|
||||||
|
SUM(CantSalida) as total_salidas,
|
||||||
|
SUM(CantEntrada) as total_devoluciones
|
||||||
|
FROM
|
||||||
|
dist_EntradasSalidasCanillas
|
||||||
|
WHERE CantSalida > 0 -- Solo considerar días con actividad de salida
|
||||||
|
GROUP BY
|
||||||
|
CAST(Fecha AS DATE), DATEPART(weekday, Fecha)
|
||||||
|
"""
|
||||||
|
print("Ejecutando consulta de agregación de datos históricos...")
|
||||||
|
df = pd.read_sql(query, cnxn)
|
||||||
|
cnxn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al conectar o consultar la base de datos: {e}")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print("No se encontraron datos históricos para entrenar el modelo de sistema. Saliendo.")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# --- 3. Feature Engineering para el modelo de sistema ---
|
||||||
|
print(f"Preparando {len(df)} registros agregados para el entrenamiento...")
|
||||||
|
# El ratio de devolución es una característica muy potente
|
||||||
|
df['ratio_devolucion'] = (df['total_devoluciones'] / df['total_salidas']).fillna(0)
|
||||||
|
# Ratio de salidas por canillita activo
|
||||||
|
df['salidas_por_canillita'] = (df['total_salidas'] / df['total_canillitas_activos']).fillna(0)
|
||||||
|
|
||||||
|
# Seleccionamos las características que el modelo usará
|
||||||
|
features = ['dia_semana', 'total_salidas', 'ratio_devolucion', 'salidas_por_canillita']
|
||||||
|
X = df[features]
|
||||||
|
|
||||||
|
# --- 4. Entrenamiento del Modelo ---
|
||||||
|
print(f"Entrenando el modelo IsolationForest de sistema con tasa de contaminación de {CONTAMINATION_RATE}...")
|
||||||
|
model = IsolationForest(
|
||||||
|
n_estimators=100,
|
||||||
|
contamination=CONTAMINATION_RATE,
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
model.fit(X)
|
||||||
|
|
||||||
|
# --- 5. Guardado del Modelo ---
|
||||||
|
joblib.dump(model, MODEL_FILE)
|
||||||
|
print("--- ENTRENAMIENTO DE SISTEMA COMPLETADO ---")
|
||||||
|
print(f"Modelo de sistema guardado exitosamente como '{MODEL_FILE}'")
|
||||||
Reference in New Issue
Block a user