Implementación AnomalIA - Fix de dropdowns y permisos.
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s

This commit is contained in:
2025-06-30 15:26:14 -03:00
parent 95aa09d62a
commit c96d259892
59 changed files with 1430 additions and 337 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
namespace GestionIntegral.Api.Dtos.Distribucion
{
public class OtroDestinoDropdownDto
{
public int IdDestino { get; set; }
public string Nombre { get; set; } = string.Empty;
}
}

View File

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

View File

@@ -0,0 +1,8 @@
namespace GestionIntegral.Api.Dtos.Impresion
{
public class EstadoBobinaDropdownDto
{
public int IdEstadoBobina { get; set; }
public string Denominacion { get; set; } = string.Empty;
}
}

View File

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

View File

@@ -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.");
}
}
}
}

View File

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

View File

@@ -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).");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
export interface CanillaDropdownDto {
idCanilla: number;
legajo?: number | null;
nomApe: string;
}

View File

@@ -0,0 +1,4 @@
export interface OtroDestinoDropdownDto {
idDestino: number;
nombre: string;
}

View File

@@ -1,4 +1,5 @@
export interface PublicacionDropdownDto {
idPublicacion: number;
nombre: string;
nombreEmpresa: string;
}

View File

@@ -0,0 +1,4 @@
export interface EstadoBobinaDropdownDto {
idEstadoBobina: number;
denominacion: string;
}

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
@@ -145,7 +146,7 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos)
newSelected.add(idPermisoSeccion);
// NO se marcan los hijos
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0 ) {
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0) {
// Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos"
newSelected.add(idPermisoSeccion); // Asegurar
permisosDelModuloHijo.forEach(p => newSelected.add(p.id));
@@ -193,7 +194,6 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
const handleGuardarCambios = async () => {
// ... (sin cambios) ...
if (!puedeAsignar || !perfil) return;
setSaving(true); setError(null); setSuccessMessage(null);
try {

View File

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

View File

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

View 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);
};

View File

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

View File

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

View File

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

View File

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

View 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 ---")

Binary file not shown.

Binary file not shown.

View 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}' ---")

View 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}'")