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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
[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}
|
||||
[HttpGet("{id:int}", Name = "GetCanillaById")]
|
||||
[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}
|
||||
[HttpGet("{id:int}", Name = "GetOtroDestinoById")]
|
||||
[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}
|
||||
[HttpGet("{id:int}", Name = "GetEstadoBobinaById")]
|
||||
[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}
|
||||
// Permiso: IB006 (Ver Tipos Bobinas)
|
||||
[HttpGet("{id:int}", Name = "GetTipoBobinaById")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System; // Para Exception
|
||||
@@ -25,7 +26,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
string? nomApeFilter,
|
||||
int? legajoFilter,
|
||||
bool? esAccionista,
|
||||
bool? soloActivos) // <<-- Parámetro aquí
|
||||
bool? soloActivos)
|
||||
{
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
var sqlBuilder = new System.Text.StringBuilder(@"
|
||||
@@ -73,6 +74,37 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
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)
|
||||
{
|
||||
const string sql = @"
|
||||
|
||||
@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public interface ICanillaRepository
|
||||
{
|
||||
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?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla
|
||||
Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction);
|
||||
|
||||
@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public interface IOtroDestinoRepository
|
||||
{
|
||||
Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter);
|
||||
Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync();
|
||||
Task<OtroDestino?> GetByIdAsync(int id);
|
||||
Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using GestionIntegral.Api.Dtos.Distribucion;
|
||||
using GestionIntegral.Api.Models.Distribucion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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)
|
||||
{
|
||||
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 GestionIntegral.Api.Data.Repositories.Impresion;
|
||||
using GestionIntegral.Api.Dtos.Impresion;
|
||||
using GestionIntegral.Api.Models.Impresion;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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)
|
||||
{
|
||||
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.Threading.Tasks;
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Impresion;
|
||||
|
||||
namespace GestionIntegral.Api.Data.Repositories.Impresion
|
||||
{
|
||||
public interface IEstadoBobinaRepository
|
||||
{
|
||||
Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter);
|
||||
Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync();
|
||||
Task<EstadoBobina?> GetByIdAsync(int id);
|
||||
Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, 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
|
||||
{
|
||||
Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter);
|
||||
Task<IEnumerable<TipoBobina>> GetAllDropdownAsync();
|
||||
Task<TipoBobina?> GetByIdAsync(int id);
|
||||
Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, 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)
|
||||
{
|
||||
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 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.Pdf;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using GestionIntegral.Api.Services.Anomalia;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -96,6 +97,8 @@ builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
|
||||
builder.Services.AddScoped<IReportesService, ReportesService>();
|
||||
// QuestPDF
|
||||
builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>();
|
||||
// Servicio de Alertas
|
||||
builder.Services.AddScoped<IAlertaService, AlertaService>();
|
||||
|
||||
// --- SERVICIO DE HEALTH CHECKS ---
|
||||
// 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;
|
||||
}
|
||||
|
||||
// CORREGIDO: MapToDto ahora acepta una tupla con tipos anulables
|
||||
private CanillaDto? MapToDto((Canilla? Canilla, string? NombreZona, string? NombreEmpresa) data)
|
||||
{
|
||||
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!);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var data = await _canillaRepository.GetByIdAsync(id);
|
||||
// MapToDto ahora devuelve CanillaDto? así que esto es correcto
|
||||
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)
|
||||
{
|
||||
return (null, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0).");
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
public interface ICanillaService
|
||||
{
|
||||
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? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, 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
|
||||
{
|
||||
Task<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter);
|
||||
Task<IEnumerable<OtroDestinoDropdownDto>> ObtenerTodosDropdownAsync();
|
||||
Task<OtroDestinoDto?> ObtenerPorIdAsync(int id);
|
||||
Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, 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);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OtroDestinoDropdownDto>> ObtenerTodosDropdownAsync()
|
||||
{
|
||||
return await _otroDestinoRepository.GetAllDropdownAsync();
|
||||
}
|
||||
|
||||
public async Task<OtroDestinoDto?> ObtenerPorIdAsync(int 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
|
||||
Nombre = d.Publicacion!.Nombre,
|
||||
NombreEmpresa = d.NombreEmpresa ?? "Empresa Desconocida",
|
||||
Habilitada = d.Publicacion!.Habilitada ?? true // Si necesitas filtrar por esto
|
||||
})
|
||||
.OrderBy(p => p.Nombre)
|
||||
|
||||
@@ -37,6 +37,12 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
return estadosBobina.Select(MapToDto);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EstadoBobinaDropdownDto>> ObtenerTodosDropdownAsync()
|
||||
{
|
||||
var estadosBobina = await _estadoBobinaRepository.GetAllDropdownAsync();
|
||||
return estadosBobina;
|
||||
}
|
||||
|
||||
public async Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id)
|
||||
{
|
||||
var estadoBobina = await _estadoBobinaRepository.GetByIdAsync(id);
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
public interface IEstadoBobinaService
|
||||
{
|
||||
Task<IEnumerable<EstadoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
|
||||
Task<IEnumerable<EstadoBobinaDropdownDto>> ObtenerTodosDropdownAsync();
|
||||
Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id);
|
||||
Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, 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
|
||||
{
|
||||
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
|
||||
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosDropdownAsync();
|
||||
Task<TipoBobinaDto?> ObtenerPorIdAsync(int id);
|
||||
Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, 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);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TipoBobinaDto>> ObtenerTodosDropdownAsync()
|
||||
{
|
||||
var tiposBobina = await _tipoBobinaRepository.GetAllDropdownAsync();
|
||||
return tiposBobina.Select(MapToDto);
|
||||
}
|
||||
|
||||
public async Task<TipoBobinaDto?> ObtenerPorIdAsync(int id)
|
||||
{
|
||||
var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(id);
|
||||
|
||||
@@ -44,6 +44,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas"
|
||||
moduloLower.includes("impresión plantas") ||
|
||||
moduloLower.includes("estados bobinas") ||
|
||||
moduloLower.includes("tipos bobinas")) { // Añadido explícitamente
|
||||
return "Impresión";
|
||||
}
|
||||
|
||||
@@ -228,15 +228,12 @@ const UsuarioFormModal: React.FC<UsuarioFormModalProps> = ({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}> {/* Fila 5 (Checkboxes) */}
|
||||
<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={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}>
|
||||
<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" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box> {/* Fin contenedor principal de campos */}
|
||||
</Box>
|
||||
|
||||
|
||||
{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 type { LoginResponseDto } from '../models/dtos/Usuarios/LoginResponseDto';
|
||||
import React, { createContext, useState, useContext, type ReactNode, useEffect, useCallback } from 'react';
|
||||
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 {
|
||||
userId: number;
|
||||
username: string;
|
||||
@@ -11,33 +10,37 @@ export interface UserContextData {
|
||||
debeCambiarClave: boolean;
|
||||
perfil: string;
|
||||
idPerfil: number;
|
||||
permissions: string[]; // Guardamos los codAcc
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
// Interfaz para el payload decodificado del JWT
|
||||
interface DecodedJwtPayload {
|
||||
sub: string; // User ID (viene como string)
|
||||
name: string; // Username
|
||||
given_name?: string; // Nombre (estándar, pero verifica tu token)
|
||||
family_name?: string; // Apellido (estándar, pero verifica tu token)
|
||||
role: string | string[]; // Puede ser uno o varios roles
|
||||
sub: string;
|
||||
name: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
role: string | string[];
|
||||
perfil: string;
|
||||
idPerfil: string; // (viene como string)
|
||||
debeCambiarClave: string; // (viene como string "True" o "False")
|
||||
permission?: string | string[]; // Nuestros claims de permiso (codAcc)
|
||||
[key: string]: any; // Para otros claims
|
||||
idPerfil: string;
|
||||
debeCambiarClave: string;
|
||||
permission?: string | string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
user: UserContextData | null; // Usar el tipo extendido
|
||||
user: UserContextData | null;
|
||||
token: string | null;
|
||||
isLoading: boolean;
|
||||
alertas: AlertaGenericaDto[];
|
||||
showForcedPasswordChangeModal: boolean;
|
||||
isPasswordChangeForced: boolean;
|
||||
|
||||
marcarAlertaComoLeida: (idAlerta: number) => Promise<void>;
|
||||
marcarGrupoDeAlertasLeido: (tipoAlerta: string, idEntidad: number) => Promise<void>;
|
||||
|
||||
setShowForcedPasswordChangeModal: (show: boolean) => 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;
|
||||
}
|
||||
|
||||
@@ -50,24 +53,57 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = 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 {
|
||||
const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken);
|
||||
|
||||
// Verificar expiración (opcional, pero buena práctica aquí también)
|
||||
const currentTime = Date.now() / 1000;
|
||||
if (decodedToken.exp && decodedToken.exp < currentTime) {
|
||||
console.warn("Token expirado al procesar.");
|
||||
logout(); // Llama a logout que limpia todo
|
||||
return;
|
||||
logout(); return;
|
||||
}
|
||||
|
||||
let permissions: string[] = [];
|
||||
if (decodedToken.permission) {
|
||||
permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission];
|
||||
}
|
||||
|
||||
const userForContext: UserContextData = {
|
||||
userId: parseInt(decodedToken.sub, 10),
|
||||
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")),
|
||||
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
|
||||
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
|
||||
permissions: permissions,
|
||||
perfil: decodedToken.perfil || 'Usuario' // Asignar un valor por defecto si no existe
|
||||
permissions: Array.isArray(decodedToken.permission) ? decodedToken.permission : (decodedToken.permission ? [decodedToken.permission] : []),
|
||||
perfil: decodedToken.perfil || 'Usuario'
|
||||
};
|
||||
|
||||
setToken(jwtToken);
|
||||
setUser(userForContext);
|
||||
setIsAuthenticated(true);
|
||||
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) {
|
||||
setShowForcedPasswordChangeModal(true);
|
||||
setIsPasswordChangeForced(true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error al decodificar o procesar token:", error);
|
||||
logout(); // Limpiar estado si el token es inválido
|
||||
console.error("Error al decodificar token:", error);
|
||||
logout();
|
||||
}
|
||||
};
|
||||
}, [logout]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
@@ -104,20 +136,18 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
processTokenAndSetUser(storedToken);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
}, [processTokenAndSetUser]);
|
||||
|
||||
const login = (apiLoginResponse: LoginResponseDto) => {
|
||||
processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido
|
||||
};
|
||||
useEffect(() => {
|
||||
if (user && isAuthenticated) {
|
||||
fetchAlertas(user);
|
||||
const intervalId = setInterval(() => fetchAlertas(user), 300000); // Refresca cada 5 mins
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [user, isAuthenticated, fetchAlertas]);
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('authUser');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
setShowForcedPasswordChangeModal(false);
|
||||
setIsPasswordChangeForced(false);
|
||||
const login = (apiLoginResponse: any) => {
|
||||
processTokenAndSetUser(apiLoginResponse.token);
|
||||
};
|
||||
|
||||
const passwordChangeCompleted = () => {
|
||||
@@ -138,6 +168,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
<AuthContext.Provider value={{
|
||||
isAuthenticated, user, token, isLoading,
|
||||
showForcedPasswordChangeModal, isPasswordChangeForced,
|
||||
alertas, marcarAlertaComoLeida, marcarGrupoDeAlertasLeido,
|
||||
setShowForcedPasswordChangeModal, passwordChangeCompleted,
|
||||
login, logout
|
||||
}}>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// src/layouts/MainLayout.tsx
|
||||
import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo
|
||||
from 'react';
|
||||
import React, { type ReactNode, useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
|
||||
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider,
|
||||
Button
|
||||
Button, Badge
|
||||
} from '@mui/material';
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||
import LockResetIcon from '@mui/icons-material/LockReset';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
@@ -18,6 +17,16 @@ interface MainLayoutProps {
|
||||
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
|
||||
const allAppModules = [
|
||||
{ label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible
|
||||
@@ -31,22 +40,36 @@ const allAppModules = [
|
||||
];
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
// Obtenemos todo lo necesario del AuthContext, INCLUYENDO LAS ALERTAS
|
||||
const {
|
||||
user, // user ya está disponible aquí
|
||||
logout,
|
||||
isAuthenticated,
|
||||
isPasswordChangeForced,
|
||||
showForcedPasswordChangeModal,
|
||||
setShowForcedPasswordChangeModal,
|
||||
passwordChangeCompleted
|
||||
user, logout, isAuthenticated, isPasswordChangeForced,
|
||||
showForcedPasswordChangeModal, setShowForcedPasswordChangeModal,
|
||||
passwordChangeCompleted,
|
||||
alertas
|
||||
} = 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 location = useLocation();
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<number | false>(false);
|
||||
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(() => {
|
||||
if (!isAuthenticated) return [];
|
||||
@@ -92,6 +115,17 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
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 = () => {
|
||||
setShowForcedPasswordChangeModal(true);
|
||||
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)
|
||||
// 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".
|
||||
@@ -162,6 +195,37 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||
)}
|
||||
{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
|
||||
size="large"
|
||||
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 {
|
||||
idPublicacion: number;
|
||||
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 { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
|
||||
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 { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
// ... (estados sin cambios) ...
|
||||
const [controles, setControles] = useState<ControlDevolucionesDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(true); // << CAMBIO: Iniciar en true
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null);
|
||||
@@ -47,42 +48,58 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CD001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CD002");
|
||||
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 => {
|
||||
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]; // Tomar solo la parte YYYY-MM-DD
|
||||
const datePart = dateString.split('T')[0];
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return datePart; // Fallback si el formato no es el esperado
|
||||
return datePart;
|
||||
};
|
||||
|
||||
|
||||
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);
|
||||
try {
|
||||
const empresasData = await empresaService.getAllEmpresas();
|
||||
const empresasData = await empresaService.getEmpresasDropdown();
|
||||
setEmpresas(empresasData);
|
||||
} catch (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.");
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false);
|
||||
}
|
||||
}, []);
|
||||
}, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia
|
||||
|
||||
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
||||
useEffect(() => {
|
||||
fetchFiltersDropdownData();
|
||||
}, [fetchFiltersDropdownData]);
|
||||
|
||||
const cargarControles = useCallback(async () => {
|
||||
// El guardián aquí ya estaba y es correcto.
|
||||
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 {
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null,
|
||||
@@ -92,19 +109,27 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
const data = await controlDevolucionesService.getAllControlesDevoluciones(params);
|
||||
setControles(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los controles de devoluciones.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa]);
|
||||
console.error(err);
|
||||
setError('Error al cargar los controles de devoluciones.');
|
||||
} 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) => {
|
||||
setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false); setEditingControl(null);
|
||||
};
|
||||
|
||||
const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
@@ -119,7 +144,6 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
setApiErrorMessage(message); throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idControl: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
@@ -133,26 +157,35 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedRow(item);
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null); setSelectedRow(null);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
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);
|
||||
|
||||
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 (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<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 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<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>)}
|
||||
</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>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */}
|
||||
{!loading && !loadingFiltersDropdown && !error && (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 300px)' }}> {/* Ajusta maxHeight */}
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
|
||||
@@ -186,7 +219,7 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{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) => (
|
||||
<TableRow key={c.idControl} hover>
|
||||
@@ -217,7 +250,10 @@ const GestionarControlDevolucionesPage: React.FC = () => {
|
||||
{puedeModificar && selectedRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{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>
|
||||
|
||||
<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 {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
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 { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
|
||||
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 EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
|
||||
@@ -44,8 +44,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
|
||||
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDropdownDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false)
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
|
||||
@@ -81,29 +81,40 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
};
|
||||
|
||||
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);
|
||||
setError(null);
|
||||
try {
|
||||
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicaciones(pubsData);
|
||||
// La carga de destinatarios se hará en el otro useEffect
|
||||
} catch (err) {
|
||||
console.error("Error cargando publicaciones para filtro:", err);
|
||||
setError("Error al cargar publicaciones.");
|
||||
} 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 () => {
|
||||
if (!puedeVer) { return; }
|
||||
|
||||
setLoadingFiltersDropdown(true);
|
||||
setFiltroIdCanillitaSeleccionado('');
|
||||
setDestinatariosDropdown([]);
|
||||
setError(null);
|
||||
try {
|
||||
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
|
||||
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter);
|
||||
const data = await canillaService.getAllDropdownCanillas(true, esAccionistaFilter);
|
||||
setDestinatariosDropdown(data);
|
||||
} catch (err) {
|
||||
console.error("Error cargando destinatarios para filtro:", err);
|
||||
@@ -111,21 +122,23 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoadingFiltersDropdown(false);
|
||||
}
|
||||
}, [filtroTipoDestinatario]);
|
||||
}, [filtroTipoDestinatario, puedeVer]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDestinatariosParaDropdown();
|
||||
}, [fetchDestinatariosParaDropdown]);
|
||||
|
||||
|
||||
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 (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const params = {
|
||||
@@ -148,6 +161,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||
cargarMovimientos();
|
||||
@@ -157,7 +171,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
if (!puedeCrear && !item) {
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
@@ -195,7 +208,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
|
||||
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -258,17 +270,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
|
||||
setOpenLiquidarDialog(false);
|
||||
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
|
||||
// Necesitamos encontrar el movimiento en la lista ANTES de recargar
|
||||
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) {
|
||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
movimientoParaTicket.fecha, // Usar la fecha del movimiento
|
||||
movimientoParaTicket.fecha,
|
||||
false
|
||||
);
|
||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||
@@ -328,7 +338,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
} finally { setLoadingTicketPdf(false); }
|
||||
}, []);
|
||||
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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]);
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) {
|
||||
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
if (!puedeVer) {
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Alert severity="error">
|
||||
{error || "No tiene permiso para acceder a esta sección."}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
|
||||
@@ -352,7 +367,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||
{/* ... (Filtros sin cambios) ... */}
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||
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 { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
|
||||
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
|
||||
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
@@ -36,8 +36,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
@@ -69,8 +69,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [pubsData, distData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
distribuidorService.getAllDistribuidores()
|
||||
publicacionService.getPublicacionesForDropdown(true),
|
||||
distribuidorService.getAllDistribuidoresDropdown()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setDistribuidores(distData);
|
||||
|
||||
@@ -16,8 +16,8 @@ import otroDestinoService from '../../services/Distribucion/otroDestinoService';
|
||||
import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto';
|
||||
import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
|
||||
import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto';
|
||||
|
||||
import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
@@ -34,8 +34,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDropdownDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
@@ -68,8 +68,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [pubsData, destinosData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
otroDestinoService.getAllOtrosDestinos()
|
||||
publicacionService.getPublicacionesForDropdown(true),
|
||||
otroDestinoService.getAllDropdownOtrosDestinos()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setOtrosDestinos(destinosData);
|
||||
|
||||
@@ -36,7 +36,6 @@ const GestionarEstadosBobinaPage: React.FC = () => {
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
// Permisos para Estados de Bobina (ej: IB010 a IB013)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("IB010");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("IB011");
|
||||
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 { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
|
||||
import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
|
||||
|
||||
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
|
||||
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 [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]);
|
||||
const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
|
||||
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDropdownDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
|
||||
@@ -76,9 +76,9 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [tiposData, plantasData, estadosData] = await Promise.all([
|
||||
tipoBobinaService.getAllTiposBobina(),
|
||||
plantaService.getAllPlantas(),
|
||||
estadoBobinaService.getAllEstadosBobina()
|
||||
tipoBobinaService.getAllDropdownTiposBobina(),
|
||||
plantaService.getPlantasForDropdown(),
|
||||
estadoBobinaService.getAllDropdownEstadosBobina()
|
||||
]);
|
||||
setTiposBobina(tiposData);
|
||||
setPlantas(plantasData);
|
||||
|
||||
@@ -18,8 +18,8 @@ import plantaService from '../../services/Impresion/plantaService'; // Para filt
|
||||
|
||||
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
|
||||
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
|
||||
|
||||
import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
@@ -36,8 +36,8 @@ const GestionarTiradasPage: React.FC = () => {
|
||||
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
|
||||
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
|
||||
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
@@ -52,8 +52,8 @@ const GestionarTiradasPage: React.FC = () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [pubsData, plantasData] = await Promise.all([
|
||||
publicacionService.getAllPublicaciones(undefined, undefined, true),
|
||||
plantaService.getAllPlantas()
|
||||
publicacionService.getPublicacionesForDropdown(true),
|
||||
plantaService.getPlantasForDropdown()
|
||||
]);
|
||||
setPublicaciones(pubsData);
|
||||
setPlantas(plantasData);
|
||||
|
||||
@@ -8,7 +8,7 @@ import SaveIcon from '@mui/icons-material/Save';
|
||||
import perfilService from '../../services/Usuarios/perfilService';
|
||||
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
|
||||
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
|
||||
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; // Renombrar para evitar conflicto
|
||||
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist';
|
||||
|
||||
@@ -46,6 +46,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
if (moduloLower.includes("impresión tiradas") ||
|
||||
moduloLower.includes("impresión bobinas") ||
|
||||
moduloLower.includes("impresión plantas") ||
|
||||
moduloLower.includes("estados bobinas") ||
|
||||
moduloLower.includes("tipos bobinas")) {
|
||||
return "Impresión";
|
||||
}
|
||||
@@ -68,7 +69,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
const { idPerfil } = useParams<{ idPerfil: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); // Renombrado
|
||||
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions();
|
||||
|
||||
const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004");
|
||||
|
||||
@@ -193,7 +194,6 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
|
||||
|
||||
|
||||
const handleGuardarCambios = async () => {
|
||||
// ... (sin cambios) ...
|
||||
if (!puedeAsignar || !perfil) return;
|
||||
setSaving(true); setError(null); setSuccessMessage(null);
|
||||
try {
|
||||
|
||||
@@ -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 { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
|
||||
const usuariosSubModules = [
|
||||
{ label: 'Perfiles', path: 'perfiles' },
|
||||
@@ -13,37 +14,54 @@ const UsuariosIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
|
||||
const { isSuperAdmin } = usePermissions();
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/usuarios';
|
||||
const subPath = location.pathname.startsWith(currentBasePath + '/')
|
||||
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta
|
||||
: (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined);
|
||||
|
||||
const activeTabIndex = usuariosSubModules.findIndex(
|
||||
(subModule) => subModule.path === subPath
|
||||
// --- 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]
|
||||
);
|
||||
|
||||
if (activeTabIndex !== -1) {
|
||||
setSelectedSubTab(activeTabIndex);
|
||||
} else {
|
||||
if (location.pathname === currentBasePath && usuariosSubModules.length > 0) {
|
||||
navigate(usuariosSubModules[0].path, { replace: true });
|
||||
// --- Ajustar la pestaña activa según la ruta ---
|
||||
useEffect(() => {
|
||||
const base = '/usuarios';
|
||||
let subPath: string | undefined;
|
||||
if (location.pathname.startsWith(base + '/')) {
|
||||
subPath = location.pathname.slice(base.length + 1).split('/')[0];
|
||||
} else if (location.pathname === base) {
|
||||
subPath = availableSubModules[0]?.path;
|
||||
}
|
||||
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]);
|
||||
}, [location.pathname, navigate, availableSubModules]);
|
||||
|
||||
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
const handleSubTabChange = (_: any, newValue: number) => {
|
||||
setSelectedSubTab(newValue);
|
||||
navigate(usuariosSubModules[newValue].path);
|
||||
navigate(availableSubModules[newValue].path);
|
||||
};
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<Tabs
|
||||
value={selectedSubTab}
|
||||
@@ -54,8 +72,8 @@ const UsuariosIndexPage: React.FC = () => {
|
||||
scrollButtons="auto"
|
||||
aria-label="sub-módulos de usuarios"
|
||||
>
|
||||
{usuariosSubModules.map((subModule) => (
|
||||
<Tab key={subModule.path} label={subModule.label} />
|
||||
{availableSubModules.map(sub => (
|
||||
<Tab key={sub.path} label={sub.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
@@ -76,6 +76,9 @@ import GestionarNovedadesCanillaPage from '../pages/Distribucion/GestionarNoveda
|
||||
import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage';
|
||||
import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage';
|
||||
|
||||
// Anonalías
|
||||
import AlertasPage from '../pages/Anomalia/AlertasPage';
|
||||
|
||||
// Auditorias
|
||||
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
|
||||
import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage';
|
||||
@@ -130,6 +133,19 @@ const AppRoutes = () => {
|
||||
{/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */}
|
||||
<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) */}
|
||||
<Route
|
||||
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 { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto';
|
||||
import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto';
|
||||
import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto';
|
||||
|
||||
|
||||
const getAllCanillas = async (
|
||||
@@ -15,12 +16,24 @@ const getAllCanillas = async (
|
||||
if (nomApeFilter) params.nomApe = nomApeFilter;
|
||||
if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter;
|
||||
if (soloActivos !== undefined) params.soloActivos = soloActivos;
|
||||
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; // <<-- ¡CLAVE! Verifica esto.
|
||||
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter;
|
||||
|
||||
const response = await apiClient.get<CanillaDto[]>('/canillas', { params });
|
||||
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 response = await apiClient.get<CanillaDto>(`/canillas/${id}`);
|
||||
return response.data;
|
||||
@@ -43,6 +56,7 @@ const toggleBajaCanilla = async (id: number, data: ToggleBajaCanillaDto): Promis
|
||||
|
||||
const canillaService = {
|
||||
getAllCanillas,
|
||||
getAllDropdownCanillas,
|
||||
getCanillaById,
|
||||
createCanilla,
|
||||
updateCanilla,
|
||||
|
||||
@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
|
||||
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
|
||||
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
|
||||
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
|
||||
import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto';
|
||||
|
||||
const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -12,6 +13,12 @@ const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDt
|
||||
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> => {
|
||||
// Llama a GET /api/otrosdestinos/{id}
|
||||
const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`);
|
||||
@@ -36,6 +43,7 @@ const deleteOtroDestino = async (id: number): Promise<void> => {
|
||||
|
||||
const otroDestinoService = {
|
||||
getAllOtrosDestinos,
|
||||
getAllDropdownOtrosDestinos,
|
||||
getOtroDestinoById,
|
||||
createOtroDestino,
|
||||
updateOtroDestino,
|
||||
|
||||
@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
|
||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto';
|
||||
import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto';
|
||||
import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
|
||||
|
||||
const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -11,6 +12,11 @@ const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoB
|
||||
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 response = await apiClient.get<EstadoBobinaDto>(`/estadosbobina/${id}`);
|
||||
return response.data;
|
||||
@@ -31,6 +37,7 @@ const deleteEstadoBobina = async (id: number): Promise<void> => {
|
||||
|
||||
const estadoBobinaService = {
|
||||
getAllEstadosBobina,
|
||||
getAllDropdownEstadosBobina,
|
||||
getEstadoBobinaById,
|
||||
createEstadoBobina,
|
||||
updateEstadoBobina,
|
||||
|
||||
@@ -12,6 +12,12 @@ const getAllTiposBobina = async (denominacionFilter?: string): Promise<TipoBobin
|
||||
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> => {
|
||||
// Llama a GET /api/tiposbobina/{id}
|
||||
const response = await apiClient.get<TipoBobinaDto>(`/tiposbobina/${id}`);
|
||||
@@ -36,6 +42,7 @@ const deleteTipoBobina = async (id: number): Promise<void> => {
|
||||
|
||||
const tipoBobinaService = {
|
||||
getAllTiposBobina,
|
||||
getAllDropdownTiposBobina,
|
||||
getTipoBobinaById,
|
||||
createTipoBobina,
|
||||
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