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) public async Task<IActionResult> GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true)
{ {
if (!TienePermiso(PermisoVer)) return Forbid(); if (!TienePermiso(PermisoVer)) return Forbid();
var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista); // <<-- Pasa el parámetro var canillitas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos, esAccionista);
return Ok(canillitas); return Ok(canillitas);
} }
[HttpGet("dropdown")]
[ProducesResponseType(typeof(IEnumerable<CanillaDropdownDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAllDropdownCanillas([FromQuery] bool? esAccionista, [FromQuery] bool? soloActivos = true)
{
var canillitas = await _canillaService.ObtenerTodosDropdownAsync(esAccionista, soloActivos);
return Ok(canillitas);
}
// GET: api/canillas/{id} // GET: api/canillas/{id}
[HttpGet("{id:int}", Name = "GetCanillaById")] [HttpGet("{id:int}", Name = "GetCanillaById")]
[ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)]

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} // GET: api/otrosdestinos/{id}
[HttpGet("{id:int}", Name = "GetOtroDestinoById")] [HttpGet("{id:int}", Name = "GetOtroDestinoById")]
[ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)] [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} // GET: api/estadosbobina/{id}
[HttpGet("{id:int}", Name = "GetEstadoBobinaById")] [HttpGet("{id:int}", Name = "GetEstadoBobinaById")]
[ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status200OK)] [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} // GET: api/tiposbobina/{id}
// Permiso: IB006 (Ver Tipos Bobinas) // Permiso: IB006 (Ver Tipos Bobinas)
[HttpGet("{id:int}", Name = "GetTipoBobinaById")] [HttpGet("{id:int}", Name = "GetTipoBobinaById")]

View File

@@ -1,4 +1,5 @@
using Dapper; using Dapper;
using GestionIntegral.Api.Dtos.Distribucion;
using GestionIntegral.Api.Models.Distribucion; using GestionIntegral.Api.Models.Distribucion;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; // Para Exception using System; // Para Exception
@@ -25,7 +26,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
string? nomApeFilter, string? nomApeFilter,
int? legajoFilter, int? legajoFilter,
bool? esAccionista, bool? esAccionista,
bool? soloActivos) // <<-- Parámetro aquí bool? soloActivos)
{ {
using var connection = _connectionFactory.CreateConnection(); using var connection = _connectionFactory.CreateConnection();
var sqlBuilder = new System.Text.StringBuilder(@" var sqlBuilder = new System.Text.StringBuilder(@"
@@ -73,6 +74,37 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
return result; return result;
} }
public async Task<IEnumerable<CanillaDropdownDto>> GetAllDropdownAsync(bool? esAccionista, bool? soloActivos)
{
using var connection = _connectionFactory.CreateConnection();
var sqlBuilder = new System.Text.StringBuilder(@"
SELECT c.Id_Canilla AS IdCanilla, c.Legajo, c.NomApe
FROM dbo.dist_dtCanillas c
WHERE 1=1 ");
var parameters = new DynamicParameters();
if (soloActivos.HasValue)
{
sqlBuilder.Append(" AND c.Baja = @BajaStatus ");
parameters.Add("BajaStatus", !soloActivos.Value); // Si soloActivos es true, Baja debe ser false
}
if (esAccionista.HasValue)
{
sqlBuilder.Append(" AND c.Accionista = @EsAccionista ");
parameters.Add("EsAccionista", esAccionista.Value); // true para accionistas, false para no accionistas (canillitas)
}
sqlBuilder.Append(" ORDER BY c.NomApe;");
var result = await connection.QueryAsync<CanillaDropdownDto>(
sqlBuilder.ToString(),
parameters
);
return result;
}
public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id) public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id)
{ {
const string sql = @" const string sql = @"

View File

@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Data; using System.Data;
using GestionIntegral.Api.Dtos.Distribucion;
namespace GestionIntegral.Api.Data.Repositories.Distribucion namespace GestionIntegral.Api.Data.Repositories.Distribucion
{ {
public interface ICanillaRepository public interface ICanillaRepository
{ {
Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista); Task<IEnumerable<(Canilla Canilla, string? NombreZona, string? NombreEmpresa)>> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos, bool? esAccionista);
Task<IEnumerable<CanillaDropdownDto>> GetAllDropdownAsync(bool? esAccionista, bool? soloActivos);
Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id); Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id);
Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla Task<Canilla?> GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla
Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction); Task<Canilla?> CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction);

View File

@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Distribucion;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Data; using System.Data;
using GestionIntegral.Api.Dtos.Distribucion;
namespace GestionIntegral.Api.Data.Repositories.Distribucion namespace GestionIntegral.Api.Data.Repositories.Distribucion
{ {
public interface IOtroDestinoRepository public interface IOtroDestinoRepository
{ {
Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter); Task<IEnumerable<OtroDestino>> GetAllAsync(string? nombreFilter);
Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync();
Task<OtroDestino?> GetByIdAsync(int id); Task<OtroDestino?> GetByIdAsync(int id);
Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction); Task<OtroDestino?> CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction); Task<bool> UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction);

View File

@@ -1,4 +1,5 @@
using Dapper; using Dapper;
using GestionIntegral.Api.Dtos.Distribucion;
using GestionIntegral.Api.Models.Distribucion; using GestionIntegral.Api.Models.Distribucion;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Generic; using System.Collections.Generic;
@@ -44,6 +45,21 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
} }
} }
public async Task<IEnumerable<OtroDestinoDropdownDto>> GetAllDropdownAsync()
{
const string sql = "SELECT Id_Destino AS IdDestino, Nombre FROM dbo.dist_dtOtrosDestinos ORDER BY Nombre;";
try
{
using var connection = _connectionFactory.CreateConnection();
return await connection.QueryAsync<OtroDestinoDropdownDto>(sql);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener Otros Destinos para dropdown.");
return Enumerable.Empty<OtroDestinoDropdownDto>();
}
}
public async Task<OtroDestino?> GetByIdAsync(int id) public async Task<OtroDestino?> GetByIdAsync(int id)
{ {
const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id"; const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id";

View File

@@ -1,5 +1,6 @@
using Dapper; using Dapper;
using GestionIntegral.Api.Data.Repositories.Impresion; using GestionIntegral.Api.Data.Repositories.Impresion;
using GestionIntegral.Api.Dtos.Impresion;
using GestionIntegral.Api.Models.Impresion; using GestionIntegral.Api.Models.Impresion;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Generic; using System.Collections.Generic;
@@ -45,6 +46,25 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion
} }
} }
public async Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync()
{
var sqlBuilder = new StringBuilder("SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion FROM dbo.bob_dtEstadosBobinas WHERE 1=1");
var parameters = new DynamicParameters();
sqlBuilder.Append(" ORDER BY Denominacion;");
try
{
using var connection = _connectionFactory.CreateConnection();
return await connection.QueryAsync<EstadoBobinaDropdownDto>(sqlBuilder.ToString(), parameters);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener todos los Estados de Bobina.");
return Enumerable.Empty<EstadoBobinaDropdownDto>();
}
}
public async Task<EstadoBobina?> GetByIdAsync(int id) public async Task<EstadoBobina?> GetByIdAsync(int id)
{ {
const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id"; const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id";

View File

@@ -2,12 +2,14 @@ using GestionIntegral.Api.Models.Impresion;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Data; using System.Data;
using GestionIntegral.Api.Dtos.Impresion;
namespace GestionIntegral.Api.Data.Repositories.Impresion namespace GestionIntegral.Api.Data.Repositories.Impresion
{ {
public interface IEstadoBobinaRepository public interface IEstadoBobinaRepository
{ {
Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter); Task<IEnumerable<EstadoBobina>> GetAllAsync(string? denominacionFilter);
Task<IEnumerable<EstadoBobinaDropdownDto>> GetAllDropdownAsync();
Task<EstadoBobina?> GetByIdAsync(int id); Task<EstadoBobina?> GetByIdAsync(int id);
Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction); Task<EstadoBobina?> CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction); Task<bool> UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction);

View File

@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Data.Repositories.Impresion
public interface ITipoBobinaRepository public interface ITipoBobinaRepository
{ {
Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter); Task<IEnumerable<TipoBobina>> GetAllAsync(string? denominacionFilter);
Task<IEnumerable<TipoBobina>> GetAllDropdownAsync();
Task<TipoBobina?> GetByIdAsync(int id); Task<TipoBobina?> GetByIdAsync(int id);
Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction); Task<TipoBobina?> CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction); Task<bool> UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction);

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) public async Task<TipoBobina?> GetByIdAsync(int id)
{ {
const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id"; const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id";

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 int IdPublicacion { get; set; }
public string Nombre { get; set; } = string.Empty; public string Nombre { get; set; } = string.Empty;
public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null public string NombreEmpresa { get; set; } = string.Empty;
public bool Habilitada { get; set; }
} }
} }

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.Reportes;
using GestionIntegral.Api.Services.Pdf; using GestionIntegral.Api.Services.Pdf;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using GestionIntegral.Api.Services.Anomalia;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -96,6 +97,8 @@ builder.Services.AddScoped<IReportesRepository, ReportesRepository>();
builder.Services.AddScoped<IReportesService, ReportesService>(); builder.Services.AddScoped<IReportesService, ReportesService>();
// QuestPDF // QuestPDF
builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>(); builder.Services.AddScoped<IQuestPdfGenerator, QuestPdfGenerator>();
// Servicio de Alertas
builder.Services.AddScoped<IAlertaService, AlertaService>();
// --- SERVICIO DE HEALTH CHECKS --- // --- SERVICIO DE HEALTH CHECKS ---
// Añadimos una comprobación específica para SQL Server. // Añadimos una comprobación específica para SQL Server.

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; _logger = logger;
} }
// CORREGIDO: MapToDto ahora acepta una tupla con tipos anulables
private CanillaDto? MapToDto((Canilla? Canilla, string? NombreZona, string? NombreEmpresa) data) private CanillaDto? MapToDto((Canilla? Canilla, string? NombreZona, string? NombreEmpresa) data)
{ {
if (data.Canilla == null) return null; if (data.Canilla == null) return null;
@@ -62,10 +61,14 @@ namespace GestionIntegral.Api.Services.Distribucion
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
} }
public async Task<IEnumerable<CanillaDropdownDto>> ObtenerTodosDropdownAsync(bool? esAccionista, bool? soloActivos)
{
return await _canillaRepository.GetAllDropdownAsync(esAccionista, soloActivos) ?? Enumerable.Empty<CanillaDropdownDto>();
}
public async Task<CanillaDto?> ObtenerPorIdAsync(int id) public async Task<CanillaDto?> ObtenerPorIdAsync(int id)
{ {
var data = await _canillaRepository.GetByIdAsync(id); var data = await _canillaRepository.GetByIdAsync(id);
// MapToDto ahora devuelve CanillaDto? así que esto es correcto
return MapToDto(data); return MapToDto(data);
} }
@@ -89,7 +92,6 @@ namespace GestionIntegral.Api.Services.Distribucion
} }
} }
// CORREGIDO: Usar directamente el valor booleano
if (createDto.Accionista == true && createDto.Empresa != 0) if (createDto.Accionista == true && createDto.Empresa != 0)
{ {
return (null, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0)."); return (null, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0).");
@@ -287,6 +289,6 @@ namespace GestionIntegral.Api.Services.Distribucion
FechaMod = h.Historial.FechaMod, FechaMod = h.Historial.FechaMod,
TipoMod = h.Historial.TipoMod TipoMod = h.Historial.TipoMod
}).ToList(); }).ToList();
} }
} }
} }

View File

@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Distribucion
public interface ICanillaService public interface ICanillaService
{ {
Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos); Task<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? esAccionista, bool? soloActivos);
Task<IEnumerable<CanillaDropdownDto>> ObtenerTodosDropdownAsync(bool? esAccionista, bool? soloActivos);
Task<CanillaDto?> ObtenerPorIdAsync(int id); Task<CanillaDto?> ObtenerPorIdAsync(int id);
Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario); Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario);

View File

@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Distribucion
public interface IOtroDestinoService public interface IOtroDestinoService
{ {
Task<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter); Task<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter);
Task<IEnumerable<OtroDestinoDropdownDto>> ObtenerTodosDropdownAsync();
Task<OtroDestinoDto?> ObtenerPorIdAsync(int id); Task<OtroDestinoDto?> ObtenerPorIdAsync(int id);
Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, int idUsuario); Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateOtroDestinoDto updateDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateOtroDestinoDto updateDto, int idUsuario);

View File

@@ -37,6 +37,11 @@ namespace GestionIntegral.Api.Services.Distribucion
return destinos.Select(MapToDto); return destinos.Select(MapToDto);
} }
public async Task<IEnumerable<OtroDestinoDropdownDto>> ObtenerTodosDropdownAsync()
{
return await _otroDestinoRepository.GetAllDropdownAsync();
}
public async Task<OtroDestinoDto?> ObtenerPorIdAsync(int id) public async Task<OtroDestinoDto?> ObtenerPorIdAsync(int id)
{ {
var destino = await _otroDestinoRepository.GetByIdAsync(id); var destino = await _otroDestinoRepository.GetByIdAsync(id);

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 IdPublicacion = d.Publicacion!.IdPublicacion, // Usar ! si estás seguro que no es null después del Where
Nombre = d.Publicacion!.Nombre, Nombre = d.Publicacion!.Nombre,
NombreEmpresa = d.NombreEmpresa ?? "Empresa Desconocida",
Habilitada = d.Publicacion!.Habilitada ?? true // Si necesitas filtrar por esto Habilitada = d.Publicacion!.Habilitada ?? true // Si necesitas filtrar por esto
}) })
.OrderBy(p => p.Nombre) .OrderBy(p => p.Nombre)

View File

@@ -37,6 +37,12 @@ namespace GestionIntegral.Api.Services.Impresion
return estadosBobina.Select(MapToDto); return estadosBobina.Select(MapToDto);
} }
public async Task<IEnumerable<EstadoBobinaDropdownDto>> ObtenerTodosDropdownAsync()
{
var estadosBobina = await _estadoBobinaRepository.GetAllDropdownAsync();
return estadosBobina;
}
public async Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id) public async Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id)
{ {
var estadoBobina = await _estadoBobinaRepository.GetByIdAsync(id); var estadoBobina = await _estadoBobinaRepository.GetByIdAsync(id);

View File

@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Impresion
public interface IEstadoBobinaService public interface IEstadoBobinaService
{ {
Task<IEnumerable<EstadoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter); Task<IEnumerable<EstadoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
Task<IEnumerable<EstadoBobinaDropdownDto>> ObtenerTodosDropdownAsync();
Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id); Task<EstadoBobinaDto?> ObtenerPorIdAsync(int id);
Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, int idUsuario); Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEstadoBobinaDto updateDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEstadoBobinaDto updateDto, int idUsuario);

View File

@@ -8,6 +8,7 @@ namespace GestionIntegral.Api.Services.Impresion
public interface ITipoBobinaService public interface ITipoBobinaService
{ {
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter); Task<IEnumerable<TipoBobinaDto>> ObtenerTodosAsync(string? denominacionFilter);
Task<IEnumerable<TipoBobinaDto>> ObtenerTodosDropdownAsync();
Task<TipoBobinaDto?> ObtenerPorIdAsync(int id); Task<TipoBobinaDto?> ObtenerPorIdAsync(int id);
Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, int idUsuario); Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoBobinaDto updateDto, int idUsuario); Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoBobinaDto updateDto, int idUsuario);

View File

@@ -36,6 +36,12 @@ namespace GestionIntegral.Api.Services.Impresion
return tiposBobina.Select(MapToDto); return tiposBobina.Select(MapToDto);
} }
public async Task<IEnumerable<TipoBobinaDto>> ObtenerTodosDropdownAsync()
{
var tiposBobina = await _tipoBobinaRepository.GetAllDropdownAsync();
return tiposBobina.Select(MapToDto);
}
public async Task<TipoBobinaDto?> ObtenerPorIdAsync(int id) public async Task<TipoBobinaDto?> ObtenerPorIdAsync(int id)
{ {
var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(id); var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(id);

View File

@@ -44,6 +44,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
if (moduloLower.includes("impresión tiradas") || if (moduloLower.includes("impresión tiradas") ||
moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas" moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas"
moduloLower.includes("impresión plantas") || moduloLower.includes("impresión plantas") ||
moduloLower.includes("estados bobinas") ||
moduloLower.includes("tipos bobinas")) { // Añadido explícitamente moduloLower.includes("tipos bobinas")) { // Añadido explícitamente
return "Impresión"; return "Impresión";
} }

View File

@@ -228,15 +228,12 @@ const UsuarioFormModal: React.FC<UsuarioFormModalProps> = ({
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}> {/* Fila 5 (Checkboxes) */} <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}>
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
<FormControlLabel control={<Checkbox checked={supAdmin} onChange={(e) => setSupAdmin(e.target.checked)} disabled={loading}/>} label="Super Administrador" />
</Box>
<Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}> <Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}>
<FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" /> <FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" />
</Box> </Box>
</Box> </Box>
</Box> {/* Fin contenedor principal de campos */} </Box>
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}

View File

@@ -1,8 +1,7 @@
import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react'; import React, { createContext, useState, useContext, type ReactNode, useEffect, useCallback } from 'react';
import type { LoginResponseDto } from '../models/dtos/Usuarios/LoginResponseDto';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
import { getAlertas, marcarAlertaLeida, marcarGrupoComoLeido, type AlertaGenericaDto } from '../services/Anomalia/alertaService';
// Interfaz para los datos del usuario que guardaremos en el contexto
export interface UserContextData { export interface UserContextData {
userId: number; userId: number;
username: string; username: string;
@@ -11,33 +10,37 @@ export interface UserContextData {
debeCambiarClave: boolean; debeCambiarClave: boolean;
perfil: string; perfil: string;
idPerfil: number; idPerfil: number;
permissions: string[]; // Guardamos los codAcc permissions: string[];
} }
// Interfaz para el payload decodificado del JWT
interface DecodedJwtPayload { interface DecodedJwtPayload {
sub: string; // User ID (viene como string) sub: string;
name: string; // Username name: string;
given_name?: string; // Nombre (estándar, pero verifica tu token) given_name?: string;
family_name?: string; // Apellido (estándar, pero verifica tu token) family_name?: string;
role: string | string[]; // Puede ser uno o varios roles role: string | string[];
perfil: string; perfil: string;
idPerfil: string; // (viene como string) idPerfil: string;
debeCambiarClave: string; // (viene como string "True" o "False") debeCambiarClave: string;
permission?: string | string[]; // Nuestros claims de permiso (codAcc) permission?: string | string[];
[key: string]: any; // Para otros claims [key: string]: any;
} }
interface AuthContextType { interface AuthContextType {
isAuthenticated: boolean; isAuthenticated: boolean;
user: UserContextData | null; // Usar el tipo extendido user: UserContextData | null;
token: string | null; token: string | null;
isLoading: boolean; isLoading: boolean;
alertas: AlertaGenericaDto[];
showForcedPasswordChangeModal: boolean; showForcedPasswordChangeModal: boolean;
isPasswordChangeForced: boolean; isPasswordChangeForced: boolean;
marcarAlertaComoLeida: (idAlerta: number) => Promise<void>;
marcarGrupoDeAlertasLeido: (tipoAlerta: string, idEntidad: number) => Promise<void>;
setShowForcedPasswordChangeModal: (show: boolean) => void; setShowForcedPasswordChangeModal: (show: boolean) => void;
passwordChangeCompleted: () => void; passwordChangeCompleted: () => void;
login: (apiLoginResponse: LoginResponseDto) => void; // Recibe el DTO de la API login: (apiLoginResponse: any) => void; // DTO no definido aquí, usamos any
logout: () => void; logout: () => void;
} }
@@ -50,24 +53,57 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false); const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false);
const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false); const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false);
const [alertas, setAlertas] = useState<AlertaGenericaDto[]>([]);
const processTokenAndSetUser = (jwtToken: string) => { const fetchAlertas = useCallback(async (currentUser: UserContextData | null) => {
if (currentUser && (currentUser.esSuperAdmin || currentUser.permissions.includes('AL001'))) {
try {
const data = await getAlertas();
setAlertas(data || []);
} catch (error) {
console.error("Error al obtener alertas en AuthContext:", error);
setAlertas([]);
}
} else {
setAlertas([]);
}
}, []);
const marcarAlertaComoLeida = async (idAlerta: number) => {
try {
await marcarAlertaLeida(idAlerta);
await fetchAlertas(user); // Refresca el estado global
} catch (error) {
console.error("Error al marcar alerta como leída:", error);
}
};
const marcarGrupoDeAlertasLeido = async (tipoAlerta: string, idEntidad: number) => {
try {
await marcarGrupoComoLeido({ tipoAlerta, idEntidad });
await fetchAlertas(user); // Refresca el estado global
} catch (error) {
console.error(`Error al marcar grupo ${tipoAlerta}/${idEntidad} como leído:`, error);
}
};
const logout = useCallback(() => {
localStorage.removeItem('authToken');
setToken(null);
setUser(null);
setIsAuthenticated(false);
setShowForcedPasswordChangeModal(false);
setIsPasswordChangeForced(false);
setAlertas([]);
}, []);
const processTokenAndSetUser = useCallback((jwtToken: string) => {
try { try {
const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken); const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken);
// Verificar expiración (opcional, pero buena práctica aquí también)
const currentTime = Date.now() / 1000; const currentTime = Date.now() / 1000;
if (decodedToken.exp && decodedToken.exp < currentTime) { if (decodedToken.exp && decodedToken.exp < currentTime) {
console.warn("Token expirado al procesar."); logout(); return;
logout(); // Llama a logout que limpia todo
return;
} }
let permissions: string[] = [];
if (decodedToken.permission) {
permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission];
}
const userForContext: UserContextData = { const userForContext: UserContextData = {
userId: parseInt(decodedToken.sub, 10), userId: parseInt(decodedToken.sub, 10),
username: decodedToken.name, username: decodedToken.name,
@@ -75,27 +111,23 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")), esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")),
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true', debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0, idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
permissions: permissions, permissions: Array.isArray(decodedToken.permission) ? decodedToken.permission : (decodedToken.permission ? [decodedToken.permission] : []),
perfil: decodedToken.perfil || 'Usuario' // Asignar un valor por defecto si no existe perfil: decodedToken.perfil || 'Usuario'
}; };
setToken(jwtToken); setToken(jwtToken);
setUser(userForContext); setUser(userForContext);
setIsAuthenticated(true); setIsAuthenticated(true);
localStorage.setItem('authToken', jwtToken); localStorage.setItem('authToken', jwtToken);
localStorage.setItem('authUser', JSON.stringify(userForContext)); // Guardar el usuario procesado
// Lógica para el modal de cambio de clave
if (userForContext.debeCambiarClave) { if (userForContext.debeCambiarClave) {
setShowForcedPasswordChangeModal(true); setShowForcedPasswordChangeModal(true);
setIsPasswordChangeForced(true); setIsPasswordChangeForced(true);
} }
} catch (error) { } catch (error) {
console.error("Error al decodificar o procesar token:", error); console.error("Error al decodificar token:", error);
logout(); // Limpiar estado si el token es inválido logout();
} }
}; }, [logout]);
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
@@ -104,20 +136,18 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
processTokenAndSetUser(storedToken); processTokenAndSetUser(storedToken);
} }
setIsLoading(false); setIsLoading(false);
}, []); }, [processTokenAndSetUser]);
const login = (apiLoginResponse: LoginResponseDto) => { useEffect(() => {
processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido if (user && isAuthenticated) {
}; fetchAlertas(user);
const intervalId = setInterval(() => fetchAlertas(user), 300000); // Refresca cada 5 mins
return () => clearInterval(intervalId);
}
}, [user, isAuthenticated, fetchAlertas]);
const logout = () => { const login = (apiLoginResponse: any) => {
localStorage.removeItem('authToken'); processTokenAndSetUser(apiLoginResponse.token);
localStorage.removeItem('authUser');
setToken(null);
setUser(null);
setIsAuthenticated(false);
setShowForcedPasswordChangeModal(false);
setIsPasswordChangeForced(false);
}; };
const passwordChangeCompleted = () => { const passwordChangeCompleted = () => {
@@ -138,6 +168,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
<AuthContext.Provider value={{ <AuthContext.Provider value={{
isAuthenticated, user, token, isLoading, isAuthenticated, user, token, isLoading,
showForcedPasswordChangeModal, isPasswordChangeForced, showForcedPasswordChangeModal, isPasswordChangeForced,
alertas, marcarAlertaComoLeida, marcarGrupoDeAlertasLeido,
setShowForcedPasswordChangeModal, passwordChangeCompleted, setShowForcedPasswordChangeModal, passwordChangeCompleted,
login, logout login, logout
}}> }}>

View File

@@ -1,14 +1,13 @@
// src/layouts/MainLayout.tsx import React, { type ReactNode, useState, useEffect, useMemo } from 'react';
import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo
from 'react';
import { import {
Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper, Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper,
IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider, IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider,
Button Button, Badge
} from '@mui/material'; } from '@mui/material';
import AccountCircle from '@mui/icons-material/AccountCircle'; import AccountCircle from '@mui/icons-material/AccountCircle';
import LockResetIcon from '@mui/icons-material/LockReset'; import LockResetIcon from '@mui/icons-material/LockReset';
import LogoutIcon from '@mui/icons-material/Logout'; import LogoutIcon from '@mui/icons-material/Logout';
import NotificationsIcon from '@mui/icons-material/Notifications';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
@@ -18,6 +17,16 @@ interface MainLayoutProps {
children: ReactNode; children: ReactNode;
} }
// --- Helper para dar nombres legibles a los tipos de alerta ---
const getTipoAlertaLabel = (tipoAlerta: string): string => {
switch (tipoAlerta) {
case 'DevolucionAnomala': return 'Devoluciones Anómalas';
case 'ComportamientoSistema': return 'Anomalías del Sistema';
case 'FaltaDeDatos': return 'Falta de Datos';
default: return tipoAlerta;
}
};
// Definición original de módulos // Definición original de módulos
const allAppModules = [ const allAppModules = [
{ label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible { label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible
@@ -31,22 +40,36 @@ const allAppModules = [
]; ];
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
// Obtenemos todo lo necesario del AuthContext, INCLUYENDO LAS ALERTAS
const { const {
user, // user ya está disponible aquí user, logout, isAuthenticated, isPasswordChangeForced,
logout, showForcedPasswordChangeModal, setShowForcedPasswordChangeModal,
isAuthenticated, passwordChangeCompleted,
isPasswordChangeForced, alertas
showForcedPasswordChangeModal,
setShowForcedPasswordChangeModal,
passwordChangeCompleted
} = useAuth(); } = useAuth();
const { tienePermiso, isSuperAdmin } = usePermissions(); // <<--- OBTENER HOOK DE PERMISOS // El resto de los hooks locales no cambian
const { tienePermiso, isSuperAdmin } = usePermissions();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [selectedTab, setSelectedTab] = useState<number | false>(false); const [selectedTab, setSelectedTab] = useState<number | false>(false);
const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null);
const [anchorElAlertasMenu, setAnchorElAlertasMenu] = useState<null | HTMLElement>(null);
// --- Agrupación de alertas para el menú ---
const gruposDeAlertas = useMemo(() => {
if (!alertas || !Array.isArray(alertas)) return [];
const groups = alertas.reduce((acc, alerta) => {
const label = getTipoAlertaLabel(alerta.tipoAlerta);
acc[label] = (acc[label] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return Object.entries(groups); // Devuelve [['Devoluciones Anómalas', 5], ...]
}, [alertas]);
const numAlertas = alertas.length;
const accessibleModules = useMemo(() => { const accessibleModules = useMemo(() => {
if (!isAuthenticated) return []; if (!isAuthenticated) return [];
@@ -92,6 +115,17 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
setAnchorElUserMenu(null); setAnchorElUserMenu(null);
}; };
// Handlers para el nuevo menú de alertas
const handleOpenAlertasMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElAlertasMenu(event.currentTarget);
};
const handleCloseAlertasMenu = () => {
setAnchorElAlertasMenu(null);
};
const handleNavigateToAlertas = () => { navigate('/anomalias/alertas'); handleCloseAlertasMenu(); };
const handleChangePasswordClick = () => { const handleChangePasswordClick = () => {
setShowForcedPasswordChangeModal(true); setShowForcedPasswordChangeModal(true);
handleCloseUserMenu(); handleCloseUserMenu();
@@ -133,7 +167,6 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
); );
} }
// Si no hay módulos accesibles después del login (y no es el cambio de clave forzado) // Si no hay módulos accesibles después del login (y no es el cambio de clave forzado)
// Esto podría pasar si un usuario no tiene permiso para NINGUNA sección, ni siquiera Inicio. // Esto podría pasar si un usuario no tiene permiso para NINGUNA sección, ni siquiera Inicio.
// Deberías redirigir a login o mostrar un mensaje de "Sin acceso". // Deberías redirigir a login o mostrar un mensaje de "Sin acceso".
@@ -162,6 +195,37 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
)} )}
{isAuthenticated && ( {isAuthenticated && (
<> <>
<IconButton onClick={handleOpenAlertasMenu} color="inherit">
<Badge badgeContent={numAlertas} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
<Menu
id="alertas-menu"
anchorEl={anchorElAlertasMenu}
open={Boolean(anchorElAlertasMenu)}
onClose={() => setAnchorElAlertasMenu(null)}
>
<MenuItem disabled>
<ListItemText primary={`Tienes ${numAlertas} alertas pendientes.`} />
</MenuItem>
<Divider />
{gruposDeAlertas.map(([label, count]) => (
<MenuItem key={label} onClick={handleNavigateToAlertas}>
<ListItemIcon><Badge badgeContent={count} color="error" sx={{mr: 2}} /></ListItemIcon>
<ListItemText>{label}</ListItemText>
</MenuItem>
))}
{numAlertas > 0 && <Divider />}
<MenuItem onClick={handleNavigateToAlertas}>
<ListItemText sx={{textAlign: 'center'}}>Ver Todas las Alertas</ListItemText>
</MenuItem>
</Menu>
<IconButton <IconButton
size="large" size="large"
aria-label="Cuenta del usuario" aria-label="Cuenta del usuario"

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 { export interface PublicacionDropdownDto {
idPublicacion: number; idPublicacion: number;
nombre: string; 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 { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto';
import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto'; import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto';
import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto'; import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal'; import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal';
import { usePermissions } from '../../hooks/usePermissions'; import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios'; import axios from 'axios';
const GestionarControlDevolucionesPage: React.FC = () => { const GestionarControlDevolucionesPage: React.FC = () => {
// ... (estados sin cambios) ...
const [controles, setControles] = useState<ControlDevolucionesDto[]>([]); const [controles, setControles] = useState<ControlDevolucionesDto[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -32,8 +33,8 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>(''); const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]); const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(true); // << CAMBIO: Iniciar en true
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null); const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null);
@@ -47,42 +48,58 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const puedeVer = isSuperAdmin || tienePermiso("CD001"); const puedeVer = isSuperAdmin || tienePermiso("CD001");
const puedeCrear = isSuperAdmin || tienePermiso("CD002"); const puedeCrear = isSuperAdmin || tienePermiso("CD002");
const puedeModificar = isSuperAdmin || tienePermiso("CD003"); const puedeModificar = isSuperAdmin || tienePermiso("CD003");
const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); // << CAMBIO: Permiso de eliminar debe ser diferente
const puedeEliminar = isSuperAdmin || tienePermiso("CD004"); // Asumiendo que CD004 es para eliminar
// CORREGIDO: Función para formatear la fecha // ... (formatDate sin cambios) ...
const formatDate = (dateString?: string | null): string => { const formatDate = (dateString?: string | null): string => {
if (!dateString) return '-'; if (!dateString) return '-';
// Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..." const datePart = dateString.split('T')[0];
const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD
const parts = datePart.split('-'); const parts = datePart.split('-');
if (parts.length === 3) { if (parts.length === 3) {
// parts[0] = YYYY, parts[1] = MM, parts[2] = DD return `${parts[2]}/${parts[1]}/${parts[0]}`;
return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY
} }
return datePart; // Fallback si el formato no es el esperado return datePart;
}; };
const fetchFiltersDropdownData = useCallback(async () => { const fetchFiltersDropdownData = useCallback(async () => {
// << CAMBIO: Guardián de permisos para la carga de filtros
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false); // Detiene el spinner principal
setLoadingFiltersDropdown(false); // Detiene el spinner de filtros
return;
}
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
const empresasData = await empresaService.getAllEmpresas(); const empresasData = await empresaService.getEmpresasDropdown();
setEmpresas(empresasData); setEmpresas(empresasData);
} catch (err) { } catch (err) {
console.error("Error cargando empresas para filtro:", err); console.error("Error cargando empresas para filtro:", err);
// El error principal se manejará en cargarControles si también falla
setError("Error al cargar opciones de filtro."); setError("Error al cargar opciones de filtro.");
} finally { } finally {
setLoadingFiltersDropdown(false); setLoadingFiltersDropdown(false);
} }
}, []); }, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); useEffect(() => {
fetchFiltersDropdownData();
}, [fetchFiltersDropdownData]);
const cargarControles = useCallback(async () => { const cargarControles = useCallback(async () => {
// El guardián aquí ya estaba y es correcto.
if (!puedeVer) { if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return; // Si ya se estableció el error en el fetch de filtros, no lo sobrescribimos.
if (!error) setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
} }
setLoading(true); setError(null); setApiErrorMessage(null);
setLoading(true);
setError(null);
setApiErrorMessage(null);
try { try {
const params = { const params = {
fechaDesde: filtroFechaDesde || null, fechaDesde: filtroFechaDesde || null,
@@ -92,19 +109,27 @@ const GestionarControlDevolucionesPage: React.FC = () => {
const data = await controlDevolucionesService.getAllControlesDevoluciones(params); const data = await controlDevolucionesService.getAllControlesDevoluciones(params);
setControles(data); setControles(data);
} catch (err) { } catch (err) {
console.error(err); setError('Error al cargar los controles de devoluciones.'); console.error(err);
} finally { setLoading(false); } setError('Error al cargar los controles de devoluciones.');
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa]); } finally {
setLoading(false);
}
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa, error]); // << CAMBIO: Añadido `error` a dependencias
useEffect(() => { cargarControles(); }, [cargarControles]); useEffect(() => {
// Solo cargar controles si los filtros se han cargado (o intentado cargar)
if (!loadingFiltersDropdown) {
cargarControles();
}
}, [cargarControles, loadingFiltersDropdown]); // << CAMBIO: Depende de la carga de filtros
// ... (resto de los handlers sin cambios) ...
const handleOpenModal = (item?: ControlDevolucionesDto) => { const handleOpenModal = (item?: ControlDevolucionesDto) => {
setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true); setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true);
}; };
const handleCloseModal = () => { const handleCloseModal = () => {
setModalOpen(false); setEditingControl(null); setModalOpen(false); setEditingControl(null);
}; };
const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => { const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => {
setApiErrorMessage(null); setApiErrorMessage(null);
try { try {
@@ -119,7 +144,6 @@ const GestionarControlDevolucionesPage: React.FC = () => {
setApiErrorMessage(message); throw err; setApiErrorMessage(message); throw err;
} }
}; };
const handleDelete = async (idControl: number) => { const handleDelete = async (idControl: number) => {
if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) { if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) {
setApiErrorMessage(null); setApiErrorMessage(null);
@@ -133,26 +157,35 @@ const GestionarControlDevolucionesPage: React.FC = () => {
} }
handleMenuClose(); handleMenuClose();
}; };
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => { const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item); setAnchorEl(event.currentTarget); setSelectedRow(item);
}; };
const handleMenuClose = () => { const handleMenuClose = () => {
setAnchorEl(null); setSelectedRow(null); setAnchorEl(null); setSelectedRow(null);
}; };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 25)); setPage(0); setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
}; };
// displayData ahora usará la 'controles' directamente, el formato se aplica en el renderizado
const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; // Si no tiene permiso, muestra solo la alerta y nada más.
if (!puedeVer) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="error">
{error || "No tiene permiso para acceder a esta sección."}
</Alert>
</Box>
);
}
return ( return (
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography> <Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography>
{/* El resto del JSX se renderizará solo si 'puedeVer' es true */}
<Paper sx={{ p: 2, mb: 2 }}> <Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
@@ -169,12 +202,12 @@ const GestionarControlDevolucionesPage: React.FC = () => {
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)} {puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)}
</Paper> </Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} {(loading || loadingFiltersDropdown) && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && ( {!loading && !loadingFiltersDropdown && !error && (
<TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */} <TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 300px)' }}> {/* Ajusta maxHeight */}
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead><TableRow> <TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell> <TableCell>Fecha</TableCell><TableCell>Empresa</TableCell>
@@ -186,7 +219,7 @@ const GestionarControlDevolucionesPage: React.FC = () => {
</TableRow></TableHead> </TableRow></TableHead>
<TableBody> <TableBody>
{displayData.length === 0 ? ( {displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow> <TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles con los filtros aplicados.</TableCell></TableRow>
) : ( ) : (
displayData.map((c) => ( displayData.map((c) => (
<TableRow key={c.idControl} hover> <TableRow key={c.idControl} hover>
@@ -217,7 +250,10 @@ const GestionarControlDevolucionesPage: React.FC = () => {
{puedeModificar && selectedRow && ( {puedeModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && ( {puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)} <MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idControl) }}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
</MenuItem>
)}
</Menu> </Menu>
<ControlDevolucionesFormModal <ControlDevolucionesFormModal

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 { import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
@@ -21,7 +21,7 @@ import canillaService from '../../services/Distribucion/canillaService';
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto'; import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto';
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal'; import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
@@ -44,8 +44,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]); const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDropdownDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false)
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null); const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
@@ -81,29 +81,40 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
const fetchPublicaciones = async () => { const fetchDropdownData = async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false); // Detiene el spinner principal
setLoadingFiltersDropdown(false); // Detiene el spinner de los filtros
return;
}
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
setError(null);
try { try {
const pubsData = await publicacionService.getPublicacionesForDropdown(true); const pubsData = await publicacionService.getPublicacionesForDropdown(true);
setPublicaciones(pubsData); setPublicaciones(pubsData);
// La carga de destinatarios se hará en el otro useEffect
} catch (err) { } catch (err) {
console.error("Error cargando publicaciones para filtro:",err); console.error("Error cargando publicaciones para filtro:", err);
setError("Error al cargar publicaciones."); setError("Error al cargar publicaciones.");
} finally { } finally {
// No setLoadingFiltersDropdown(false) acá, esperar a la otra carga // La carga finaliza cuando se cargan los destinatarios también.
} }
}; };
fetchPublicaciones(); fetchDropdownData();
}, []); }, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia
const fetchDestinatariosParaDropdown = useCallback(async () => { const fetchDestinatariosParaDropdown = useCallback(async () => {
if (!puedeVer) { return; }
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
setFiltroIdCanillitaSeleccionado(''); setFiltroIdCanillitaSeleccionado('');
setDestinatariosDropdown([]); setDestinatariosDropdown([]);
setError(null); setError(null);
try { try {
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas'; const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter); const data = await canillaService.getAllDropdownCanillas(true, esAccionistaFilter);
setDestinatariosDropdown(data); setDestinatariosDropdown(data);
} catch (err) { } catch (err) {
console.error("Error cargando destinatarios para filtro:", err); console.error("Error cargando destinatarios para filtro:", err);
@@ -111,21 +122,23 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
} finally { } finally {
setLoadingFiltersDropdown(false); setLoadingFiltersDropdown(false);
} }
}, [filtroTipoDestinatario]); }, [filtroTipoDestinatario, puedeVer]);
useEffect(() => { useEffect(() => {
fetchDestinatariosParaDropdown(); fetchDestinatariosParaDropdown();
}, [fetchDestinatariosParaDropdown]); }, [fetchDestinatariosParaDropdown]);
const cargarMovimientos = useCallback(async () => { const cargarMovimientos = useCallback(async () => {
if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; } if (!puedeVer) {
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
if (!filtroFecha || !filtroIdCanillitaSeleccionado) { if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
if (loading) setLoading(false); if (loading) setLoading(false);
setMovimientos([]); setMovimientos([]);
return; return;
} }
setLoading(true); setError(null); setApiErrorMessage(null); setLoading(true); setError(null); setApiErrorMessage(null);
try { try {
const params = { const params = {
@@ -148,6 +161,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
} }
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]); }, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
useEffect(() => { useEffect(() => {
if (filtroFecha && filtroIdCanillitaSeleccionado) { if (filtroFecha && filtroIdCanillitaSeleccionado) {
cargarMovimientos(); cargarMovimientos();
@@ -156,8 +170,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
if (loading) setLoading(false); if (loading) setLoading(false);
} }
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); }, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
if (!puedeCrear && !item) { if (!puedeCrear && !item) {
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos."); setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
@@ -195,7 +208,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
} }
handleMenuClose(); handleMenuClose();
}; };
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => { const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
event.currentTarget.setAttribute('data-rowid', item.idParte.toString()); event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -258,17 +270,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto); await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
setOpenLiquidarDialog(false); setOpenLiquidarDialog(false);
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0]; const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
// Necesitamos encontrar el movimiento en la lista ANTES de recargar
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado); const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
await cargarMovimientos(); // Recargar la lista para reflejar el estado liquidado await cargarMovimientos();
// Usar la fecha del movimiento original para el ticket
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) { if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla); console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
await handleImprimirTicketLiquidacion( await handleImprimirTicketLiquidacion(
movimientoParaTicket.idCanilla, movimientoParaTicket.idCanilla,
movimientoParaTicket.fecha, // Usar la fecha del movimiento movimientoParaTicket.fecha,
false false
); );
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) { } else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
@@ -328,7 +338,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
} finally { setLoadingTicketPdf(false); } } finally { setLoadingTicketPdf(false); }
}, []); }, []);
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
@@ -339,8 +348,14 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0) displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0)
, [displayData]); , [displayData]);
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { if (!puedeVer) {
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; return (
<Box sx={{ p: 2 }}>
<Alert severity="error">
{error || "No tiene permiso para acceder a esta sección."}
</Alert>
</Box>
);
} }
const numSelectedToLiquidate = selectedIdsParaLiquidar.size; const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
@@ -352,7 +367,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<Paper sx={{ p: 2, mb: 2 }}> <Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
{/* ... (Filtros sin cambios) ... */}
<TextField label="Fecha" type="date" size="small" value={filtroFecha} <TextField label="Fecha" type="date" size="small" value={filtroFecha}
onChange={(e) => setFiltroFecha(e.target.value)} onChange={(e) => setFiltroFecha(e.target.value)}
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}

View File

@@ -17,8 +17,8 @@ import distribuidorService from '../../services/Distribucion/distribuidorService
import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto'; import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto';
import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto'; import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto';
import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto'; import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal'; import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal';
import { usePermissions } from '../../hooks/usePermissions'; import { usePermissions } from '../../hooks/usePermissions';
@@ -36,8 +36,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>(''); const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>(''); const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]); const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
@@ -69,8 +69,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => {
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
const [pubsData, distData] = await Promise.all([ const [pubsData, distData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true), publicacionService.getPublicacionesForDropdown(true),
distribuidorService.getAllDistribuidores() distribuidorService.getAllDistribuidoresDropdown()
]); ]);
setPublicaciones(pubsData); setPublicaciones(pubsData);
setDistribuidores(distData); setDistribuidores(distData);

View File

@@ -16,8 +16,8 @@ import otroDestinoService from '../../services/Distribucion/otroDestinoService';
import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto'; import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto';
import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto'; import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto';
import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto'; import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto';
import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal'; import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal';
import { usePermissions } from '../../hooks/usePermissions'; import { usePermissions } from '../../hooks/usePermissions';
@@ -34,8 +34,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>(''); const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]); const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDropdownDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
@@ -68,8 +68,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => {
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
const [pubsData, destinosData] = await Promise.all([ const [pubsData, destinosData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true), publicacionService.getPublicacionesForDropdown(true),
otroDestinoService.getAllOtrosDestinos() otroDestinoService.getAllDropdownOtrosDestinos()
]); ]);
setPublicaciones(pubsData); setPublicaciones(pubsData);
setOtrosDestinos(destinosData); setOtrosDestinos(destinosData);

View File

@@ -36,7 +36,6 @@ const GestionarEstadosBobinaPage: React.FC = () => {
const { tienePermiso, isSuperAdmin } = usePermissions(); const { tienePermiso, isSuperAdmin } = usePermissions();
// Permisos para Estados de Bobina (ej: IB010 a IB013)
const puedeVer = isSuperAdmin || tienePermiso("IB010"); const puedeVer = isSuperAdmin || tienePermiso("IB010");
const puedeCrear = isSuperAdmin || tienePermiso("IB011"); const puedeCrear = isSuperAdmin || tienePermiso("IB011");
const puedeModificar = isSuperAdmin || tienePermiso("IB012"); const puedeModificar = isSuperAdmin || tienePermiso("IB012");

View File

@@ -21,8 +21,8 @@ import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateSto
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto'; import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto'; import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal'; import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal'; import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
@@ -50,8 +50,8 @@ const GestionarStockBobinasPage: React.FC = () => {
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]); const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]); const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]); const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDropdownDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [ingresoModalOpen, setIngresoModalOpen] = useState(false); const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
@@ -76,9 +76,9 @@ const GestionarStockBobinasPage: React.FC = () => {
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
const [tiposData, plantasData, estadosData] = await Promise.all([ const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(), tipoBobinaService.getAllDropdownTiposBobina(),
plantaService.getAllPlantas(), plantaService.getPlantasForDropdown(),
estadoBobinaService.getAllEstadosBobina() estadoBobinaService.getAllDropdownEstadosBobina()
]); ]);
setTiposBobina(tiposData); setTiposBobina(tiposData);
setPlantas(plantasData); setPlantas(plantasData);

View File

@@ -18,8 +18,8 @@ import plantaService from '../../services/Impresion/plantaService'; // Para filt
import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto'; import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto';
import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto'; import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal'; import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal';
import { usePermissions } from '../../hooks/usePermissions'; import { usePermissions } from '../../hooks/usePermissions';
@@ -36,8 +36,8 @@ const GestionarTiradasPage: React.FC = () => {
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>(''); const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>('');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]); const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
@@ -52,8 +52,8 @@ const GestionarTiradasPage: React.FC = () => {
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
const [pubsData, plantasData] = await Promise.all([ const [pubsData, plantasData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true), publicacionService.getPublicacionesForDropdown(true),
plantaService.getAllPlantas() plantaService.getPlantasForDropdown()
]); ]);
setPublicaciones(pubsData); setPublicaciones(pubsData);
setPlantas(plantasData); setPlantas(plantasData);

View File

@@ -8,67 +8,68 @@ import SaveIcon from '@mui/icons-material/Save';
import perfilService from '../../services/Usuarios/perfilService'; import perfilService from '../../services/Usuarios/perfilService';
import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto'; import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto';
import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto';
import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; // Renombrar para evitar conflicto import { usePermissions as usePagePermissions } from '../../hooks/usePermissions';
import axios from 'axios'; import axios from 'axios';
import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist';
const SECCION_PERMISSIONS_PREFIX = "SS"; const SECCION_PERMISSIONS_PREFIX = "SS";
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => { const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
if (codAcc === "SS001") return "Distribución"; if (codAcc === "SS001") return "Distribución";
if (codAcc === "SS002") return "Contables"; if (codAcc === "SS002") return "Contables";
if (codAcc === "SS003") return "Impresión"; if (codAcc === "SS003") return "Impresión";
if (codAcc === "SS004") return "Reportes"; if (codAcc === "SS004") return "Reportes";
if (codAcc === "SS005") return "Radios"; if (codAcc === "SS005") return "Radios";
if (codAcc === "SS006") return "Usuarios"; if (codAcc === "SS006") return "Usuarios";
return null; return null;
}; };
const getModuloConceptualDelPermiso = (permisoModulo: string): string => { const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
const moduloLower = permisoModulo.toLowerCase(); const moduloLower = permisoModulo.toLowerCase();
if (moduloLower.includes("distribuidores") || if (moduloLower.includes("distribuidores") ||
moduloLower.includes("canillas") || moduloLower.includes("canillas") ||
moduloLower.includes("publicaciones distribución") || moduloLower.includes("publicaciones distribución") ||
moduloLower.includes("zonas distribuidores") || moduloLower.includes("zonas distribuidores") ||
moduloLower.includes("movimientos distribuidores") || moduloLower.includes("movimientos distribuidores") ||
moduloLower.includes("empresas") || moduloLower.includes("empresas") ||
moduloLower.includes("otros destinos") || moduloLower.includes("otros destinos") ||
moduloLower.includes("ctrl. devoluciones") || moduloLower.includes("ctrl. devoluciones") ||
moduloLower.includes("movimientos canillas") || moduloLower.includes("movimientos canillas") ||
moduloLower.includes("salidas otros destinos")) { moduloLower.includes("salidas otros destinos")) {
return "Distribución"; return "Distribución";
} }
if (moduloLower.includes("cuentas pagos") || if (moduloLower.includes("cuentas pagos") ||
moduloLower.includes("cuentas notas") || moduloLower.includes("cuentas notas") ||
moduloLower.includes("cuentas tipos pagos")) { moduloLower.includes("cuentas tipos pagos")) {
return "Contables"; return "Contables";
} }
if (moduloLower.includes("impresión tiradas") || if (moduloLower.includes("impresión tiradas") ||
moduloLower.includes("impresión bobinas") || moduloLower.includes("impresión bobinas") ||
moduloLower.includes("impresión plantas") || moduloLower.includes("impresión plantas") ||
moduloLower.includes("tipos bobinas")) { moduloLower.includes("estados bobinas") ||
return "Impresión"; moduloLower.includes("tipos bobinas")) {
} return "Impresión";
if (moduloLower.includes("radios")) { }
return "Radios"; if (moduloLower.includes("radios")) {
} return "Radios";
if (moduloLower.includes("usuarios") || }
moduloLower.includes("perfiles")) { if (moduloLower.includes("usuarios") ||
return "Usuarios"; moduloLower.includes("perfiles")) {
} return "Usuarios";
if (moduloLower.includes("reportes")) { }
return "Reportes"; if (moduloLower.includes("reportes")) {
} return "Reportes";
if (moduloLower.includes("permisos")) { }
return "Permisos (Definición)"; if (moduloLower.includes("permisos")) {
} return "Permisos (Definición)";
return permisoModulo; }
return permisoModulo;
}; };
const AsignarPermisosAPerfilPage: React.FC = () => { const AsignarPermisosAPerfilPage: React.FC = () => {
const { idPerfil } = useParams<{ idPerfil: string }>(); const { idPerfil } = useParams<{ idPerfil: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); // Renombrado const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions();
const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004"); const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004");
@@ -124,76 +125,75 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo
) => { ) => {
setPermisosSeleccionados(prevSelected => { setPermisosSeleccionados(prevSelected => {
const newSelected = new Set(prevSelected); const newSelected = new Set(prevSelected);
const permisoActual = permisosDisponibles.find(p => p.id === permisoId); const permisoActual = permisosDisponibles.find(p => p.id === permisoId);
if (!permisoActual) return prevSelected; if (!permisoActual) return prevSelected;
const permisosDelModuloHijo = moduloConceptualAsociado const permisosDelModuloHijo = moduloConceptualAsociado
? permisosDisponibles.filter(p => { ? permisosDisponibles.filter(p => {
const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper
return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX); return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX);
}) })
: []; : [];
if (esPermisoSeccionClick && moduloConceptualAsociado) { if (esPermisoSeccionClick && moduloConceptualAsociado) {
const idPermisoSeccion = permisoActual.id; const idPermisoSeccion = permisoActual.id;
const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion); const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion);
const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id)); const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id));
const ningunHijoEstabaSeleccionado = permisosDelModuloHijo.every(p => !prevSelected.has(p.id)); const ningunHijoEstabaSeleccionado = permisosDelModuloHijo.every(p => !prevSelected.has(p.id));
if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos) if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos)
newSelected.add(idPermisoSeccion); newSelected.add(idPermisoSeccion);
// NO se marcan los hijos // NO se marcan los hijos
} else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0 ) { } else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0) {
// Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos" // Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos"
newSelected.add(idPermisoSeccion); // Asegurar newSelected.add(idPermisoSeccion); // Asegurar
permisosDelModuloHijo.forEach(p => newSelected.add(p.id)); permisosDelModuloHijo.forEach(p => newSelected.add(p.id));
} else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off } else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off
newSelected.delete(idPermisoSeccion); newSelected.delete(idPermisoSeccion);
permisosDelModuloHijo.forEach(p => newSelected.delete(p.id)); permisosDelModuloHijo.forEach(p => newSelected.delete(p.id));
}
} else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo
if (asignadoViaCheckboxHijo) {
newSelected.add(permisoId);
const permisoSeccionPadre = permisosDisponibles.find(
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
);
if (permisoSeccionPadre && !newSelected.has(permisoSeccionPadre.id)) {
newSelected.add(permisoSeccionPadre.id); // Marcar padre si no estaba
}
} else { // Desmarcando un hijo
newSelected.delete(permisoId);
const permisoSeccionPadre = permisosDisponibles.find(
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
);
if (permisoSeccionPadre) {
const algunOtroHijoSeleccionado = permisosDelModuloHijo.some(p => p.id !== permisoId && newSelected.has(p.id));
if (!algunOtroHijoSeleccionado && newSelected.has(permisoSeccionPadre.id)) {
// Si era el último hijo y el padre estaba marcado, NO desmarcamos el padre automáticamente.
// El estado indeterminate se encargará visualmente.
// Si quisiéramos que se desmarque el padre, aquí iría: newSelected.delete(permisoSeccionPadre.id);
}
}
}
} else { // Permiso sin módulo conceptual asociado (ej: "Permisos (Definición)")
if (asignadoViaCheckboxHijo) {
newSelected.add(permisoId);
} else {
newSelected.delete(permisoId);
}
} }
if (successMessage) setSuccessMessage(null); } else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo
if (error) setError(null); if (asignadoViaCheckboxHijo) {
return newSelected; newSelected.add(permisoId);
const permisoSeccionPadre = permisosDisponibles.find(
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
);
if (permisoSeccionPadre && !newSelected.has(permisoSeccionPadre.id)) {
newSelected.add(permisoSeccionPadre.id); // Marcar padre si no estaba
}
} else { // Desmarcando un hijo
newSelected.delete(permisoId);
const permisoSeccionPadre = permisosDisponibles.find(
ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado
);
if (permisoSeccionPadre) {
const algunOtroHijoSeleccionado = permisosDelModuloHijo.some(p => p.id !== permisoId && newSelected.has(p.id));
if (!algunOtroHijoSeleccionado && newSelected.has(permisoSeccionPadre.id)) {
// Si era el último hijo y el padre estaba marcado, NO desmarcamos el padre automáticamente.
// El estado indeterminate se encargará visualmente.
// Si quisiéramos que se desmarque el padre, aquí iría: newSelected.delete(permisoSeccionPadre.id);
}
}
}
} else { // Permiso sin módulo conceptual asociado (ej: "Permisos (Definición)")
if (asignadoViaCheckboxHijo) {
newSelected.add(permisoId);
} else {
newSelected.delete(permisoId);
}
}
if (successMessage) setSuccessMessage(null);
if (error) setError(null);
return newSelected;
}); });
}, [permisosDisponibles, successMessage, error]); }, [permisosDisponibles, successMessage, error]);
const handleGuardarCambios = async () => { const handleGuardarCambios = async () => {
// ... (sin cambios) ...
if (!puedeAsignar || !perfil) return; if (!puedeAsignar || !perfil) return;
setSaving(true); setError(null); setSuccessMessage(null); setSaving(true); setError(null); setSuccessMessage(null);
try { try {
@@ -214,54 +214,54 @@ const AsignarPermisosAPerfilPage: React.FC = () => {
}; };
if (loading) { if (loading) {
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>; return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
} }
if (error && !perfil) { if (error && !perfil) {
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>; return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
} }
if (!puedeAsignar) { if (!puedeAsignar) {
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>; return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
} }
if (!perfil && !loading) { if (!perfil && !loading) {
return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>; return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>;
} }
return ( return (
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}> <Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}>
Volver a Perfiles Volver a Perfiles
</Button> </Button>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'}
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" gutterBottom> <Typography variant="body2" color="textSecondary" gutterBottom>
ID Perfil: {perfil?.id} ID Perfil: {perfil?.id}
</Typography> </Typography>
{error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} {error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
{successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>} {successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>}
<Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}> <Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}>
<PermisosChecklist <PermisosChecklist
permisosDisponibles={permisosDisponibles} permisosDisponibles={permisosDisponibles}
permisosSeleccionados={permisosSeleccionados} permisosSeleccionados={permisosSeleccionados}
onPermisoChange={handlePermisoChange} onPermisoChange={handlePermisoChange}
disabled={saving} disabled={saving}
/> />
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />} startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
onClick={handleGuardarCambios} onClick={handleGuardarCambios}
disabled={saving || !puedeAsignar} disabled={saving || !puedeAsignar}
> >
Guardar Cambios Guardar Cambios
</Button> </Button>
</Box>
</Paper>
</Box> </Box>
); </Paper>
</Box>
);
}; };
export default AsignarPermisosAPerfilPage; export default AsignarPermisosAPerfilPage;

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 { Box, Tabs, Tab, Paper, Typography } from '@mui/material';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { usePermissions } from '../../hooks/usePermissions';
const usuariosSubModules = [ const usuariosSubModules = [
{ label: 'Perfiles', path: 'perfiles' }, { label: 'Perfiles', path: 'perfiles' },
@@ -13,37 +14,54 @@ const UsuariosIndexPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false); const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false);
const { isSuperAdmin } = usePermissions();
// --- Filtrar solo lo que puede ver este usuario ---
const availableSubModules = useMemo(
() =>
usuariosSubModules.filter(sub => {
// Estos dos ítems solo para superadmins
if (
(sub.path === 'permisos' || sub.path === 'auditoria-usuarios')
&& !isSuperAdmin
) {
return false;
}
return true;
}),
[isSuperAdmin]
);
// --- Ajustar la pestaña activa según la ruta ---
useEffect(() => { useEffect(() => {
const currentBasePath = '/usuarios'; const base = '/usuarios';
const subPath = location.pathname.startsWith(currentBasePath + '/') let subPath: string | undefined;
? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta if (location.pathname.startsWith(base + '/')) {
: (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined); subPath = location.pathname.slice(base.length + 1).split('/')[0];
} else if (location.pathname === base) {
const activeTabIndex = usuariosSubModules.findIndex( subPath = availableSubModules[0]?.path;
(subModule) => subModule.path === subPath
);
if (activeTabIndex !== -1) {
setSelectedSubTab(activeTabIndex);
} else {
if (location.pathname === currentBasePath && usuariosSubModules.length > 0) {
navigate(usuariosSubModules[0].path, { replace: true });
setSelectedSubTab(0);
} else {
setSelectedSubTab(false);
}
} }
}, [location.pathname, navigate]); const idx = availableSubModules.findIndex(m => m.path === subPath);
if (idx !== -1) {
setSelectedSubTab(idx);
} else if (location.pathname === base && availableSubModules.length) {
navigate(availableSubModules[0].path, { replace: true });
setSelectedSubTab(0);
} else {
setSelectedSubTab(false);
}
}, [location.pathname, navigate, availableSubModules]);
const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { const handleSubTabChange = (_: any, newValue: number) => {
setSelectedSubTab(newValue); setSelectedSubTab(newValue);
navigate(usuariosSubModules[newValue].path); navigate(availableSubModules[newValue].path);
}; };
return ( return (
<Box> <Box>
<Typography variant="h5" gutterBottom>Módulo de Usuarios y Seguridad</Typography> <Typography variant="h5" gutterBottom>
Módulo de Usuarios y Seguridad
</Typography>
<Paper square elevation={1}> <Paper square elevation={1}>
<Tabs <Tabs
value={selectedSubTab} value={selectedSubTab}
@@ -54,8 +72,8 @@ const UsuariosIndexPage: React.FC = () => {
scrollButtons="auto" scrollButtons="auto"
aria-label="sub-módulos de usuarios" aria-label="sub-módulos de usuarios"
> >
{usuariosSubModules.map((subModule) => ( {availableSubModules.map(sub => (
<Tab key={subModule.path} label={subModule.label} /> <Tab key={sub.path} label={sub.label} />
))} ))}
</Tabs> </Tabs>
</Paper> </Paper>
@@ -66,4 +84,4 @@ const UsuariosIndexPage: React.FC = () => {
); );
}; };
export default UsuariosIndexPage; export default UsuariosIndexPage;

View File

@@ -76,6 +76,9 @@ import GestionarNovedadesCanillaPage from '../pages/Distribucion/GestionarNoveda
import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage'; import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage';
import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage'; import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage';
// Anonalías
import AlertasPage from '../pages/Anomalia/AlertasPage';
// Auditorias // Auditorias
import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage'; import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage';
import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage'; import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage';
@@ -130,6 +133,19 @@ const AppRoutes = () => {
{/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */} {/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */}
<Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */} <Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */}
{/* Módulo de Anomalías */}
<Route
path="anomalias"
element={
<SectionProtectedRoute requiredPermission="AL001" sectionName="Anomalías">
<Outlet />
</SectionProtectedRoute>
}
>
<Route index element={<Navigate to="alertas" replace />} />
<Route path="alertas" element={<AlertasPage />} />
</Route>
{/* Módulo de Distribución (anidado) */} {/* Módulo de Distribución (anidado) */}
<Route <Route
path="distribucion" path="distribucion"

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 { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto';
import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto'; import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto';
import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto'; import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto';
import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto';
const getAllCanillas = async ( const getAllCanillas = async (
@@ -15,12 +16,24 @@ const getAllCanillas = async (
if (nomApeFilter) params.nomApe = nomApeFilter; if (nomApeFilter) params.nomApe = nomApeFilter;
if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter; if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter;
if (soloActivos !== undefined) params.soloActivos = soloActivos; if (soloActivos !== undefined) params.soloActivos = soloActivos;
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; // <<-- ¡CLAVE! Verifica esto. if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter;
const response = await apiClient.get<CanillaDto[]>('/canillas', { params }); const response = await apiClient.get<CanillaDto[]>('/canillas', { params });
return response.data; return response.data;
}; };
const getAllDropdownCanillas = async (
soloActivos?: boolean,
esAccionistaFilter?: boolean // Asegúrate que esté aquí
): Promise<CanillaDropdownDto[]> => {
const params: Record<string, string | number | boolean> = {};
if (soloActivos !== undefined) params.soloActivos = soloActivos;
if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter;
const response = await apiClient.get<CanillaDropdownDto[]>('/canillas/dropdown', { params });
return response.data;
};
const getCanillaById = async (id: number): Promise<CanillaDto> => { const getCanillaById = async (id: number): Promise<CanillaDto> => {
const response = await apiClient.get<CanillaDto>(`/canillas/${id}`); const response = await apiClient.get<CanillaDto>(`/canillas/${id}`);
return response.data; return response.data;
@@ -43,6 +56,7 @@ const toggleBajaCanilla = async (id: number, data: ToggleBajaCanillaDto): Promis
const canillaService = { const canillaService = {
getAllCanillas, getAllCanillas,
getAllDropdownCanillas,
getCanillaById, getCanillaById,
createCanilla, createCanilla,
updateCanilla, updateCanilla,

View File

@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto';
import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto';
import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto'; import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto';
import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto';
const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => { const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
@@ -12,6 +13,12 @@ const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDt
return response.data; return response.data;
}; };
const getAllDropdownOtrosDestinos = async (): Promise<OtroDestinoDto[]> => {
// Llama a GET /api/otrosdestinos/dropdown
const response = await apiClient.get<OtroDestinoDropdownDto[]>('/otrosdestinos/dropdown');
return response.data;
};
const getOtroDestinoById = async (id: number): Promise<OtroDestinoDto> => { const getOtroDestinoById = async (id: number): Promise<OtroDestinoDto> => {
// Llama a GET /api/otrosdestinos/{id} // Llama a GET /api/otrosdestinos/{id}
const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`); const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`);
@@ -36,6 +43,7 @@ const deleteOtroDestino = async (id: number): Promise<void> => {
const otroDestinoService = { const otroDestinoService = {
getAllOtrosDestinos, getAllOtrosDestinos,
getAllDropdownOtrosDestinos,
getOtroDestinoById, getOtroDestinoById,
createOtroDestino, createOtroDestino,
updateOtroDestino, updateOtroDestino,

View File

@@ -2,6 +2,7 @@ import apiClient from '../apiClient';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto';
import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto';
import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => { const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
@@ -11,6 +12,11 @@ const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoB
return response.data; return response.data;
}; };
const getAllDropdownEstadosBobina = async (): Promise<EstadoBobinaDropdownDto[]> => {
const response = await apiClient.get<EstadoBobinaDropdownDto[]>('/estadosbobina/dropdown');
return response.data;
};
const getEstadoBobinaById = async (id: number): Promise<EstadoBobinaDto> => { const getEstadoBobinaById = async (id: number): Promise<EstadoBobinaDto> => {
const response = await apiClient.get<EstadoBobinaDto>(`/estadosbobina/${id}`); const response = await apiClient.get<EstadoBobinaDto>(`/estadosbobina/${id}`);
return response.data; return response.data;
@@ -31,6 +37,7 @@ const deleteEstadoBobina = async (id: number): Promise<void> => {
const estadoBobinaService = { const estadoBobinaService = {
getAllEstadosBobina, getAllEstadosBobina,
getAllDropdownEstadosBobina,
getEstadoBobinaById, getEstadoBobinaById,
createEstadoBobina, createEstadoBobina,
updateEstadoBobina, updateEstadoBobina,

View File

@@ -12,6 +12,12 @@ const getAllTiposBobina = async (denominacionFilter?: string): Promise<TipoBobin
return response.data; return response.data;
}; };
const getAllDropdownTiposBobina = async (): Promise<TipoBobinaDto[]> => {
// Llama a GET /api/tiposbobina/dropdown
const response = await apiClient.get<TipoBobinaDto[]>('/tiposbobina/dropdown');
return response.data;
};
const getTipoBobinaById = async (id: number): Promise<TipoBobinaDto> => { const getTipoBobinaById = async (id: number): Promise<TipoBobinaDto> => {
// Llama a GET /api/tiposbobina/{id} // Llama a GET /api/tiposbobina/{id}
const response = await apiClient.get<TipoBobinaDto>(`/tiposbobina/${id}`); const response = await apiClient.get<TipoBobinaDto>(`/tiposbobina/${id}`);
@@ -36,6 +42,7 @@ const deleteTipoBobina = async (id: number): Promise<void> => {
const tipoBobinaService = { const tipoBobinaService = {
getAllTiposBobina, getAllTiposBobina,
getAllDropdownTiposBobina,
getTipoBobinaById, getTipoBobinaById,
createTipoBobina, createTipoBobina,
updateTipoBobina, updateTipoBobina,

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