diff --git a/Backend/GestionIntegral.Api/Controllers/AuthController.cs b/Backend/GestionIntegral.Api/Controllers/AuthController.cs index 21855f0..30f0d5a 100644 --- a/Backend/GestionIntegral.Api/Controllers/AuthController.cs +++ b/Backend/GestionIntegral.Api/Controllers/AuthController.cs @@ -1,6 +1,8 @@ using GestionIntegral.Api.Dtos; using GestionIntegral.Api.Services; +using Microsoft.AspNetCore.Authorization; // Para [Authorize] using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; // Para leer claims del token namespace GestionIntegral.Api.Controllers { @@ -50,7 +52,49 @@ namespace GestionIntegral.Api.Controllers } } - // TODO: Añadir endpoint para cambiar clave [HttpPost("change-password")] - // Probablemente requerirá [Authorize] para que solo usuarios logueados puedan usarlo. + [HttpPost("change-password")] + [Authorize] // <-- Solo usuarios autenticados pueden cambiar su clave + [ProducesResponseType(StatusCodes.Status204NoContent)] // Éxito sin contenido + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] // Si el token es inválido + [ProducesResponseType(StatusCodes.Status404NotFound)] // Si el usuario del token no existe + public async Task ChangePassword([FromBody] ChangePasswordRequestDto changePasswordRequest) + { + if (!ModelState.IsValid) + { + // El [ApiController] y el [FromBody] ya validan DTOs (Required, StringLength, Compare) + // y devuelven 400 automáticamente si falla. + return BadRequest(ModelState); + } + + // Obtener el ID del usuario desde el token JWT + var userIdString = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); // "sub" es el claim estándar para ID + if (!int.TryParse(userIdString, out int userId)) + { + _logger.LogWarning("ChangePassword failed: Could not parse UserId from token."); + // Esto no debería pasar si el token es válido y generado por nosotros + return Unauthorized(new { message = "Token de usuario inválido." }); + } + + try + { + var success = await _authService.ChangePasswordAsync(userId, changePasswordRequest); + + if (!success) + { + // AuthService ya loggeó la razón específica (usuario no encontrado, clave actual inválida, etc.) + // Devolvemos un BadRequest genérico para no dar pistas + return BadRequest(new { message = "No se pudo cambiar la contraseña. Verifique la contraseña actual." }); + } + + // Éxito + return NoContent(); // Código 204: Éxito, sin contenido que devolver + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during password change for user ID {UserId}", userId); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Ocurrió un error interno al cambiar la contraseña." }); + } + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Contables/TiposPagoController.cs b/Backend/GestionIntegral.Api/Controllers/Contables/TiposPagoController.cs new file mode 100644 index 0000000..b77b910 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Contables/TiposPagoController.cs @@ -0,0 +1,177 @@ +using GestionIntegral.Api.Dtos.Contables; // Para los DTOs +using GestionIntegral.Api.Services.Contables; // Para ITipoPagoService +using Microsoft.AspNetCore.Authorization; // Para [Authorize] +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; // Para obtener el IdUsuario del token + +namespace GestionIntegral.Api.Controllers.Contables +{ + [Route("api/[controller]")] // Ruta base: /api/tipospago + [ApiController] + [Authorize] // <-- Proteger todos los endpoints de este controlador por defecto + public class TiposPagoController : ControllerBase + { + private readonly ITipoPagoService _tipoPagoService; + private readonly ILogger _logger; + + public TiposPagoController(ITipoPagoService tipoPagoService, ILogger logger) + { + _tipoPagoService = tipoPagoService; + _logger = logger; + } + + // GET: api/tipospago + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + // Podrías añadir un permiso específico si lo tienes, ej: [Authorize(Policy = "VerTiposPago")] + // Por ahora, [Authorize] a nivel de controlador es suficiente si todos los métodos lo requieren. + public async Task GetAllTiposPago([FromQuery] string? nombre) // Filtro opcional por nombre + { + try + { + var tiposPago = await _tipoPagoService.ObtenerTodosAsync(nombre); + return Ok(tiposPago); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Tipos de Pago."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // GET: api/tipospago/{id} + [HttpGet("{id:int}", Name = "GetTipoPagoById")] // Darle un nombre a la ruta para CreatedAtRoute + [ProducesResponseType(typeof(TipoPagoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetTipoPagoById(int id) + { + try + { + var tipoPago = await _tipoPagoService.ObtenerPorIdAsync(id); + if (tipoPago == null) + { + return NotFound(new { message = $"Tipo de pago con ID {id} no encontrado." }); + } + return Ok(tipoPago); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Tipo de Pago por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // POST: api/tipospago + [HttpPost] + [ProducesResponseType(typeof(TipoPagoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + // Aquí podrías requerir un permiso más específico: [Authorize(Policy = "CrearTiposPago")] + public async Task CreateTipoPago([FromBody] CreateTipoPagoDto createDto) + { + if (!ModelState.IsValid) // Validaciones del DTO (Required, StringLength) + { + return BadRequest(ModelState); + } + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + + try + { + var (tipoPagoCreado, error) = await _tipoPagoService.CrearAsync(createDto, idUsuario.Value); + + if (error != null) + { + // Error de lógica de negocio (ej: nombre duplicado) + return BadRequest(new { message = error }); + } + if (tipoPagoCreado == null) // Fallo inesperado en el repositorio/servicio + { + return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el tipo de pago."); + } + + // Devuelve 201 Created con la ubicación del nuevo recurso y el recurso mismo + return CreatedAtRoute("GetTipoPagoById", new { id = tipoPagoCreado.IdTipoPago }, tipoPagoCreado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Tipo de Pago. Nombre: {Nombre}", createDto.Nombre); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // PUT: api/tipospago/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] // Éxito sin contenido + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateTipoPago(int id, [FromBody] UpdateTipoPagoDto updateDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _tipoPagoService.ActualizarAsync(id, updateDto, idUsuario.Value); + + if (!exito) + { + if (error == "Tipo de pago no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Otro error de lógica de negocio + } + return NoContent(); // Éxito + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Tipo de Pago ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // DELETE: api/tipospago/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteTipoPago(int id) + { + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _tipoPagoService.EliminarAsync(id, idUsuario.Value); + + if (!exito) + { + if (error == "Tipo de pago no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Ej: "En uso" + } + return NoContent(); // Éxito + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Tipo de Pago ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // Helper para obtener el ID del usuario del token + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) + { + return userId; + } + _logger.LogWarning("No se pudo obtener el UserId del token JWT."); + return null; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/AuthRepository.cs b/Backend/GestionIntegral.Api/Data/AuthRepository.cs index d365c2f..326fd84 100644 --- a/Backend/GestionIntegral.Api/Data/AuthRepository.cs +++ b/Backend/GestionIntegral.Api/Data/AuthRepository.cs @@ -7,18 +7,21 @@ namespace GestionIntegral.Api.Data public class AuthRepository : IAuthRepository { private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; - public AuthRepository(DbConnectionFactory connectionFactory) + public AuthRepository(DbConnectionFactory connectionFactory, ILogger logger) { _connectionFactory = connectionFactory; + _logger = logger; + } public async Task GetUserByUsernameAsync(string username) { var sql = @"SELECT Id, [User], ClaveHash, ClaveSalt, Habilitada, - SupAdmin, Nombre, Apellido, IdPerfil, VerLog, DebeCambiarClave - FROM gral_Usuarios - WHERE [User] = @Username"; + SupAdmin, Nombre, Apellido, IdPerfil, VerLog, DebeCambiarClave + FROM gral_Usuarios + WHERE [User] = @Username"; try { @@ -30,13 +33,81 @@ namespace GestionIntegral.Api.Data } catch (Exception ex) { - // Loggear el error ex.Message - Console.WriteLine($"Error fetching user: {ex.Message}"); + Console.WriteLine($"Error fetching user by username: {ex.Message}"); return null; } } - // TODO: Implementar métodos para cambiar clave (UPDATE seguro con parámetros) - // y para crear usuario (INSERT seguro con parámetros, usando el hasher) + public async Task GetUserByIdAsync(int userId) + { + var sql = @"SELECT Id, [User], ClaveHash, ClaveSalt, Habilitada, + SupAdmin, Nombre, Apellido, IdPerfil, VerLog, DebeCambiarClave + FROM gral_Usuarios + WHERE Id = @UserId"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var user = await connection.QuerySingleOrDefaultAsync(sql, new { UserId = userId }); + Console.WriteLine($"Repo - User {user?.Id} - DebeCambiarClave leído de BD: {user?.DebeCambiarClave}"); + return user; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching user by ID: {ex.Message}"); + return null; + } + } + + public async Task UpdatePasswordAsync(int userId, string newHash, string newSalt) + { + // Actualiza hash, salt y pone DebeCambiarClave a 0 (false) + var sql = @"UPDATE dbo.gral_Usuarios + SET ClaveHash = @HashedPassword, ClaveSalt = @Salt, DebeCambiarClave = 0 + WHERE Id = @UserId"; + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var parameters = new + { + HashedPassword = newHash, + Salt = newSalt, + UserId = userId + }; + int rowsAffected = await connection.ExecuteAsync(sql, parameters); + return rowsAffected == 1; // Devuelve true si se actualizó exactamente una fila + } + } + catch (Exception ex) + { + Console.WriteLine($"Error updating password for user {userId}: {ex.Message}"); + return false; + } + } + public async Task> GetPermisosCodAccByPerfilIdAsync(int idPerfil) + { + // Esta consulta es similar a la que tenías en gestion.vb -> verPermisosPerfil + var sql = @" + SELECT p.codAcc + FROM dbo.gral_Perfiles pf + INNER JOIN dbo.gral_PermisosPerfiles pp ON pf.id = pp.idPerfil + INNER JOIN dbo.gral_Permisos p ON pp.idPermiso = p.id + WHERE pf.id = @IdPerfil;"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QueryAsync(sql, new { IdPerfil = idPerfil }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener códigos de acceso para el perfil ID: {IdPerfil}", idPerfil); // Asumiendo que tienes _logger en AuthRepository + return Enumerable.Empty(); + } + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/IAuthRepository.cs b/Backend/GestionIntegral.Api/Data/IAuthRepository.cs index f3063b0..9ee32c0 100644 --- a/Backend/GestionIntegral.Api/Data/IAuthRepository.cs +++ b/Backend/GestionIntegral.Api/Data/IAuthRepository.cs @@ -5,6 +5,8 @@ namespace GestionIntegral.Api.Data public interface IAuthRepository { Task GetUserByUsernameAsync(string username); - // Añadiremos métodos para cambiar clave, etc., más adelante + Task UpdatePasswordAsync(int userId, string newHash, string newSalt); + Task GetUserByIdAsync(int userId); // Método útil para cambio de clav + Task> GetPermisosCodAccByPerfilIdAsync(int idPerfil); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/ITipoPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/ITipoPagoRepository.cs new file mode 100644 index 0000000..e8f7c7b --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/ITipoPagoRepository.cs @@ -0,0 +1,17 @@ +using GestionIntegral.Api.Models.Contables; // Para TipoPago y TipoPagoHistorico +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories +{ + public interface ITipoPagoRepository + { + Task> GetAllAsync(string? nombreFilter); + Task GetByIdAsync(int id); + Task CreateAsync(TipoPago nuevoTipoPago, int idUsuario); // Devuelve el objeto creado o null si falla + Task UpdateAsync(TipoPago tipoPagoAActualizar, int idUsuario); + Task DeleteAsync(int id, int idUsuario); // Devuelve true si fue exitoso + Task ExistsByNameAsync(string nombre, int? excludeId = null); + Task IsInUseAsync(int id); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/TipoPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/TipoPagoRepository.cs new file mode 100644 index 0000000..6fc8d77 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/TipoPagoRepository.cs @@ -0,0 +1,288 @@ +using Dapper; +using GestionIntegral.Api.Models.Contables; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories // O GestionIntegral.Api.Repositories +{ + public class TipoPagoRepository : ITipoPagoRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; // Para logging + + public TipoPagoRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter) + { + // Construcción segura de la cláusula WHERE + var sqlBuilder = new System.Text.StringBuilder("SELECT Id_TipoPago AS IdTipoPago, Nombre, Detalle FROM dbo.cue_dtTipopago WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND Nombre LIKE @NombreFilter"); + parameters.Add("NombreFilter", $"%{nombreFilter}%"); + } + sqlBuilder.Append(" ORDER BY Nombre;"); + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Tipos de Pago. Filtro: {NombreFilter}", nombreFilter); + return Enumerable.Empty(); // Devolver lista vacía en caso de error + } + } + + public async Task GetByIdAsync(int id) + { + var sql = "SELECT Id_TipoPago AS IdTipoPago, Nombre, Detalle FROM dbo.cue_dtTipopago WHERE Id_TipoPago = @Id"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Tipo de Pago por ID: {IdTipoPago}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombre, int? excludeId = null) + { + var sqlBuilder = new System.Text.StringBuilder("SELECT COUNT(1) FROM dbo.cue_dtTipopago WHERE Nombre = @Nombre"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_TipoPago != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync para Tipo de Pago con nombre: {Nombre}", nombre); + return true; // Asumir que existe para prevenir duplicados si hay error de BD + } + } + + public async Task CreateAsync(TipoPago nuevoTipoPago, int idUsuario) + { + var sqlInsertPrincipal = @" + INSERT INTO dbo.cue_dtTipopago (Nombre, Detalle) + VALUES (@Nombre, @Detalle); + SELECT CAST(SCOPE_IDENTITY() as int);"; // Obtener el ID generado + + var sqlInsertHistorico = @" + INSERT INTO dbo.cue_dtTipopago_H (Id_TipoPago, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTipoPago, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + // Insertar en la tabla principal y obtener el ID + int nuevoId = await connection.ExecuteScalarAsync( + sqlInsertPrincipal, + new { nuevoTipoPago.Nombre, nuevoTipoPago.Detalle }, + transaction: transaction + ); + + if (nuevoId > 0) + { + nuevoTipoPago.IdTipoPago = nuevoId; // Asignar el ID al objeto + + // Insertar en la tabla de historial + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdTipoPago = nuevoId, // Usar el ID obtenido + nuevoTipoPago.Nombre, + nuevoTipoPago.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction: transaction); + + transaction.Commit(); + return nuevoTipoPago; // Devolver el objeto completo con ID + } + else + { + transaction.Rollback(); + _logger.LogError("SCOPE_IDENTITY() devolvió 0 o menos después de insertar TipoPago. Nombre: {Nombre}", nuevoTipoPago.Nombre); + return null; + } + } + catch (Exception exTrans) + { + transaction.Rollback(); + _logger.LogError(exTrans, "Error en transacción CreateAsync para TipoPago. Nombre: {Nombre}", nuevoTipoPago.Nombre); + return null; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error general en CreateAsync para TipoPago. Nombre: {Nombre}", nuevoTipoPago.Nombre); + return null; + } + } + + public async Task UpdateAsync(TipoPago tipoPagoAActualizar, int idUsuario) + { + // Primero, obtenemos el estado actual para el historial + var tipoPagoActual = await GetByIdAsync(tipoPagoAActualizar.IdTipoPago); + if (tipoPagoActual == null) return false; // No se encontró para actualizar + + var sqlUpdate = @" + UPDATE dbo.cue_dtTipopago + SET Nombre = @Nombre, Detalle = @Detalle + WHERE Id_TipoPago = @IdTipoPago;"; + + var sqlInsertHistorico = @" + INSERT INTO dbo.cue_dtTipopago_H (Id_TipoPago, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTipoPago, @NombreActual, @DetalleActual, @IdUsuario, @FechaMod, @TipoMod);"; + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + // Insertar en historial CON LOS VALORES ANTERIORES + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdTipoPago = tipoPagoActual.IdTipoPago, + NombreActual = tipoPagoActual.Nombre, // Valor antes del update + DetalleActual = tipoPagoActual.Detalle, // Valor antes del update + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction: transaction); + + // Actualizar la tabla principal + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new + { + tipoPagoAActualizar.Nombre, + tipoPagoAActualizar.Detalle, + tipoPagoAActualizar.IdTipoPago + }, transaction: transaction); + + transaction.Commit(); + return rowsAffected == 1; + } + catch (Exception exTrans) + { + transaction.Rollback(); + _logger.LogError(exTrans, "Error en transacción UpdateAsync para TipoPago ID: {IdTipoPago}", tipoPagoAActualizar.IdTipoPago); + return false; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error general en UpdateAsync para TipoPago ID: {IdTipoPago}", tipoPagoAActualizar.IdTipoPago); + return false; + } + } + + public async Task DeleteAsync(int id, int idUsuario) + { + var tipoPagoAEliminar = await GetByIdAsync(id); + if (tipoPagoAEliminar == null) return false; // No existe + + var sqlDelete = "DELETE FROM dbo.cue_dtTipopago WHERE Id_TipoPago = @Id"; + var sqlInsertHistorico = @" + INSERT INTO dbo.cue_dtTipopago_H (Id_TipoPago, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTipoPago, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + // Insertar en historial ANTES de eliminar + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdTipoPago = tipoPagoAEliminar.IdTipoPago, + tipoPagoAEliminar.Nombre, + tipoPagoAEliminar.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + transaction.Commit(); + return rowsAffected == 1; + } + catch (Exception exTrans) + { + transaction.Rollback(); + _logger.LogError(exTrans, "Error en transacción DeleteAsync para TipoPago ID: {IdTipoPago}", id); + return false; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error general en DeleteAsync para TipoPago ID: {IdTipoPago}", id); + return false; + } + } + + public async Task IsInUseAsync(int id) + { + var sql = "SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Id_TipoPago = @IdTipoPago"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var count = await connection.ExecuteScalarAsync(sql, new { IdTipoPago = id }); + return count > 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para TipoPago ID: {IdTipoPago}", id); + return true; // Asumir que está en uso si hay error para prevenir borrado incorrecto + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/TipoPago.cs b/Backend/GestionIntegral.Api/Models/Contables/TipoPago.cs new file mode 100644 index 0000000..2665a5d --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/TipoPago.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Models.Contables +{ + public class TipoPago + { + public int IdTipoPago { get; set; } // Coincide con la PK de cue_dtTipopago + public string Nombre { get; set; } = string.Empty; + public string? Detalle { get; set; } // Permite nulo + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/TipoPagoHistorico.cs b/Backend/GestionIntegral.Api/Models/Contables/TipoPagoHistorico.cs new file mode 100644 index 0000000..ccdef40 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/TipoPagoHistorico.cs @@ -0,0 +1,12 @@ +namespace GestionIntegral.Api.Models.Contables +{ + public class TipoPagoHistorico + { + public int IdTipoPago { get; set; } // Este NO es IDENTITY + public string Nombre { get; set; } = string.Empty; + public string? Detalle { get; set; } + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/ChangePasswordRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/ChangePasswordRequestDto.cs new file mode 100644 index 0000000..943a3fd --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/ChangePasswordRequestDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos +{ + public class ChangePasswordRequestDto + { + [Required] + public string CurrentPassword { get; set; } = string.Empty; + + [Required] + [StringLength(50, MinimumLength = 6)] // Validaciones + public string NewPassword { get; set; } = string.Empty; + + [Required] + [Compare("NewPassword", ErrorMessage = "La nueva contraseña y la confirmación no coinciden.")] // Validación de confirmación + public string ConfirmNewPassword { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateTipoPagoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateTipoPagoDto.cs new file mode 100644 index 0000000..6efc1ee --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateTipoPagoDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Contables +{ + public class CreateTipoPagoDto + { + [Required(ErrorMessage = "El nombre del tipo de pago es obligatorio.")] + [StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "El detalle no puede exceder los 150 caracteres.")] + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/TipoPagoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/TipoPagoDto.cs new file mode 100644 index 0000000..931d330 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/TipoPagoDto.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Dtos.Contables +{ + public class TipoPagoDto + { + public int IdTipoPago { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateTipoPagoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateTipoPagoDto.cs new file mode 100644 index 0000000..f423f03 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateTipoPagoDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Contables +{ + public class UpdateTipoPagoDto + { + [Required(ErrorMessage = "El nombre del tipo de pago es obligatorio.")] + [StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "El detalle no puede exceder los 150 caracteres.")] + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index f06f009..c81e178 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -3,14 +3,18 @@ using Microsoft.IdentityModel.Tokens; using System.Text; using GestionIntegral.Api.Data; using GestionIntegral.Api.Services; +using GestionIntegral.Api.Services.Contables; +using GestionIntegral.Api.Data.Repositories; var builder = WebApplication.CreateBuilder(args); // --- Registros de Servicios --- builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services.AddScoped(); // Asegúrate que el namespace sea correcto +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // --- Configuración de Autenticación JWT --- var jwtSettings = builder.Configuration.GetSection("Jwt"); diff --git a/Backend/GestionIntegral.Api/Services/AuthService.cs b/Backend/GestionIntegral.Api/Services/AuthService.cs index ea2eb47..dd65b99 100644 --- a/Backend/GestionIntegral.Api/Services/AuthService.cs +++ b/Backend/GestionIntegral.Api/Services/AuthService.cs @@ -34,34 +34,35 @@ namespace GestionIntegral.Api.Services if (user == null) { - _logger.LogWarning("Login attempt failed: User {Username} not found.", loginRequest.Username); - return null; + _logger.LogWarning("Login attempt failed: User {Username} not found.", loginRequest.Username); + return null; } - if (!user.Habilitada) { - _logger.LogWarning("Login attempt failed: User {Username} is disabled.", loginRequest.Username); - return null; + _logger.LogWarning("Login attempt failed: User {Username} is disabled.", loginRequest.Username); + return null; } - // Verificar contraseña usando el hash y salt de la BD bool isPasswordValid = _passwordHasher.VerifyPassword(loginRequest.Password, user.ClaveHash, user.ClaveSalt); - if (!isPasswordValid) { - _logger.LogWarning("Login attempt failed: Invalid password for user {Username}.", loginRequest.Username); - return null; + _logger.LogWarning("Login attempt failed: Invalid password for user {Username}.", loginRequest.Username); + return null; } - // Generar Token JWT - var token = GenerateJwtToken(user); + // --- OBTENER PERMISOS --- + IEnumerable permisosDelUsuario = new List(); + if (!user.SupAdmin && user.IdPerfil > 0) // Solo si no es SuperAdmin y tiene un perfil válido + { + permisosDelUsuario = await _authRepository.GetPermisosCodAccByPerfilIdAsync(user.IdPerfil); + } + // --- FIN OBTENER PERMISOS --- - // Determinar si debe cambiar clave (leyendo de la BD via el repo/modelo) + var token = GenerateJwtToken(user, permisosDelUsuario); // Pasar permisos bool debeCambiar = user.DebeCambiarClave; - _logger.LogInformation("User {Username} logged in successfully.", loginRequest.Username); + _logger.LogInformation("User {Username} logged in successfully.", loginRequest.Username); - // Crear y devolver la respuesta return new LoginResponseDto { Token = token, @@ -70,30 +71,96 @@ namespace GestionIntegral.Api.Services NombreCompleto = $"{user.Nombre} {user.Apellido}", EsSuperAdmin = user.SupAdmin, DebeCambiarClave = debeCambiar + // No necesitamos pasar la lista de permisos aquí, ya irán en el token }; } + public async Task ChangePasswordAsync(int userId, ChangePasswordRequestDto changePasswordRequest) + { + // 1. Obtener el usuario actual (necesitamos su hash/salt actual) + var user = await _authRepository.GetUserByIdAsync(userId); + if (user == null) + { + _logger.LogWarning("ChangePassword attempt failed: User with ID {UserId} not found.", userId); + return false; // Usuario no encontrado + } + + // 2. Verificar la contraseña ACTUAL + if (!_passwordHasher.VerifyPassword(changePasswordRequest.CurrentPassword, user.ClaveHash, user.ClaveSalt)) + { + _logger.LogWarning("ChangePassword attempt failed: Invalid current password for user ID {UserId}.", userId); + return false; // Contraseña actual incorrecta + } + + // 3. Verificar que la nueva contraseña no sea igual al nombre de usuario + if (user.User == changePasswordRequest.NewPassword) + { + _logger.LogWarning("ChangePassword attempt failed: New password cannot be the same as username for user ID {UserId}.", userId); + // Podrías lanzar una excepción o devolver un código de error específico + // Por simplicidad, devolvemos false + return false; + } + + // 4. Generar nuevo hash y salt para la NUEVA contraseña + (string newHash, string newSalt) = _passwordHasher.HashPassword(changePasswordRequest.NewPassword); + + // 5. Actualizar en la base de datos (el repositorio también pone DebeCambiarClave = 0) + bool success = await _authRepository.UpdatePasswordAsync(userId, newHash, newSalt); + + if (success) + { + _logger.LogInformation("Password changed successfully for user ID {UserId}.", userId); + } + else + { + _logger.LogError("Failed to update password in database for user ID {UserId}.", userId); + } + + return success; + } + // --- GenerateJwtToken sin cambios --- - private string GenerateJwtToken(Usuario user) + private string GenerateJwtToken(Usuario user, IEnumerable permisosCodAcc) // Recibir permisos { var jwtSettings = _configuration.GetSection("Jwt"); var key = Encoding.ASCII.GetBytes(jwtSettings["Key"] ?? throw new ArgumentNullException("Jwt:Key", "JWT Key not configured")); + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new Claim(JwtRegisteredClaimNames.Name, user.User), + new Claim(JwtRegisteredClaimNames.GivenName, user.Nombre), + new Claim(JwtRegisteredClaimNames.FamilyName, user.Apellido), + new Claim("idPerfil", user.IdPerfil.ToString()), + new Claim("debeCambiarClave", user.DebeCambiarClave.ToString()), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + // Añadir rol basado en SupAdmin o IdPerfil + if (user.SupAdmin) + { + claims.Add(new Claim(ClaimTypes.Role, "SuperAdmin")); + // Opcional: Si SuperAdmin tiene todos los permisos, podrías añadir un claim especial + // claims.Add(new Claim("permission", "*")); + } + else + { + claims.Add(new Claim(ClaimTypes.Role, $"Perfil_{user.IdPerfil}")); + // Añadir cada código de permiso como un claim "permission" + if (permisosCodAcc != null) + { + foreach (var codAcc in permisosCodAcc) + { + claims.Add(new Claim("permission", codAcc)); // Usar "permission" como tipo de claim + } + } + } + var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor { - Subject = new ClaimsIdentity(new[] - { - new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new Claim(JwtRegisteredClaimNames.Name, user.User), - new Claim(JwtRegisteredClaimNames.GivenName, user.Nombre), - new Claim(JwtRegisteredClaimNames.FamilyName, user.Apellido), - new Claim(ClaimTypes.Role, user.SupAdmin ? "SuperAdmin" : $"Perfil_{user.IdPerfil}"), - new Claim("idPerfil", user.IdPerfil.ToString()), // Añadir IdPerfil como claim explícito - new Claim("debeCambiarClave", user.DebeCambiarClave.ToString()), // Añadir flag como claim - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }), + Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddHours(Convert.ToInt32(jwtSettings["DurationInHours"] ?? "1")), Issuer = jwtSettings["Issuer"], Audience = jwtSettings["Audience"], @@ -103,7 +170,5 @@ namespace GestionIntegral.Api.Services var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } - - // TODO: Implementar ChangePasswordAsync } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/ITipoPagoService.cs b/Backend/GestionIntegral.Api/Services/Contables/ITipoPagoService.cs new file mode 100644 index 0000000..3dc82ae --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/ITipoPagoService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Contables; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Contables +{ + public interface ITipoPagoService + { + Task> ObtenerTodosAsync(string? nombreFilter); + Task ObtenerPorIdAsync(int id); + Task<(TipoPagoDto? TipoPago, string? Error)> CrearAsync(CreateTipoPagoDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoPagoDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/TipoPagoService.cs b/Backend/GestionIntegral.Api/Services/Contables/TipoPagoService.cs new file mode 100644 index 0000000..50eeb44 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/TipoPagoService.cs @@ -0,0 +1,134 @@ +using GestionIntegral.Api.Data.Repositories; // Para ITipoPagoRepository +using GestionIntegral.Api.Dtos.Contables; +using GestionIntegral.Api.Models.Contables; // Para TipoPago +using GestionIntegral.Api.Services.Contables; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Contables +{ + public class TipoPagoService : ITipoPagoService + { + private readonly ITipoPagoRepository _tipoPagoRepository; + private readonly ILogger _logger; + + public TipoPagoService(ITipoPagoRepository tipoPagoRepository, ILogger logger) + { + _tipoPagoRepository = tipoPagoRepository; + _logger = logger; + } + + public async Task> ObtenerTodosAsync(string? nombreFilter) + { + var tiposPago = await _tipoPagoRepository.GetAllAsync(nombreFilter); + // Mapeo de Entidad a DTO + return tiposPago.Select(tp => new TipoPagoDto + { + IdTipoPago = tp.IdTipoPago, + Nombre = tp.Nombre, + Detalle = tp.Detalle + }); + } + + public async Task ObtenerPorIdAsync(int id) + { + var tipoPago = await _tipoPagoRepository.GetByIdAsync(id); + if (tipoPago == null) return null; + + // Mapeo de Entidad a DTO + return new TipoPagoDto + { + IdTipoPago = tipoPago.IdTipoPago, + Nombre = tipoPago.Nombre, + Detalle = tipoPago.Detalle + }; + } + + public async Task<(TipoPagoDto? TipoPago, string? Error)> CrearAsync(CreateTipoPagoDto createDto, int idUsuario) + { + // Validación: Nombre no puede estar duplicado + if (await _tipoPagoRepository.ExistsByNameAsync(createDto.Nombre)) + { + return (null, "El nombre del tipo de pago ya existe."); + } + + var nuevoTipoPago = new TipoPago + { + Nombre = createDto.Nombre, + Detalle = createDto.Detalle + }; + + var tipoPagoCreado = await _tipoPagoRepository.CreateAsync(nuevoTipoPago, idUsuario); + + if (tipoPagoCreado == null) + { + _logger.LogError("Falló la creación del Tipo de Pago en el repositorio para el nombre: {Nombre}", createDto.Nombre); + return (null, "Error al crear el tipo de pago en la base de datos."); + } + + // Mapeo de Entidad creada (con ID) a DTO + var tipoPagoDto = new TipoPagoDto + { + IdTipoPago = tipoPagoCreado.IdTipoPago, + Nombre = tipoPagoCreado.Nombre, + Detalle = tipoPagoCreado.Detalle + }; + + return (tipoPagoDto, null); // Éxito + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoPagoDto updateDto, int idUsuario) + { + // Verificar si el tipo de pago existe + var tipoPagoExistente = await _tipoPagoRepository.GetByIdAsync(id); + if (tipoPagoExistente == null) + { + return (false, "Tipo de pago no encontrado."); + } + + // Validación: Nombre no puede estar duplicado (excluyendo el ID actual) + if (await _tipoPagoRepository.ExistsByNameAsync(updateDto.Nombre, id)) + { + return (false, "El nombre del tipo de pago ya existe para otro registro."); + } + + // Mapeo de DTO a Entidad para actualizar + tipoPagoExistente.Nombre = updateDto.Nombre; + tipoPagoExistente.Detalle = updateDto.Detalle; + + var actualizado = await _tipoPagoRepository.UpdateAsync(tipoPagoExistente, idUsuario); + + if (!actualizado) + { + _logger.LogError("Falló la actualización del Tipo de Pago en el repositorio para el ID: {Id}", id); + return (false, "Error al actualizar el tipo de pago en la base de datos."); + } + return (true, null); // Éxito + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + // Verificar si el tipo de pago existe + var tipoPagoExistente = await _tipoPagoRepository.GetByIdAsync(id); + if (tipoPagoExistente == null) + { + return (false, "Tipo de pago no encontrado."); + } + + // Validación: No se puede eliminar si está en uso + if (await _tipoPagoRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. El tipo de pago está siendo utilizado en pagos registrados."); + } + + var eliminado = await _tipoPagoRepository.DeleteAsync(id, idUsuario); + if (!eliminado) + { + _logger.LogError("Falló la eliminación del Tipo de Pago en el repositorio para el ID: {Id}", id); + return (false, "Error al eliminar el tipo de pago de la base de datos."); + } + return (true, null); // Éxito + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/IAuthService.cs b/Backend/GestionIntegral.Api/Services/IAuthService.cs index f0bff7c..5965dc3 100644 --- a/Backend/GestionIntegral.Api/Services/IAuthService.cs +++ b/Backend/GestionIntegral.Api/Services/IAuthService.cs @@ -5,6 +5,6 @@ namespace GestionIntegral.Api.Services public interface IAuthService { Task LoginAsync(LoginRequestDto loginRequest); - // Añadiremos cambio de clave, etc. + Task ChangePasswordAsync(int userId, ChangePasswordRequestDto changePasswordRequest); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs index a910bf6..e9e6822 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+9b1de95404118dad24e3e848866c48fce6e0c08e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+da7b544372d72fd6e4a82a3d95626e9cd273f4a4")] [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index ba4f424..d0fdb6c 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -13,12 +13,14 @@ "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", "axios": "^1.9.0", + "jwt-decode": "^4.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.5.3" }, "devDependencies": { "@eslint/js": "^9.25.0", + "@types/jwt-decode": "^2.2.1", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", @@ -60,9 +62,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "dev": true, "license": "MIT", "engines": { @@ -100,6 +102,13 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", @@ -117,13 +126,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.1", + "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -217,9 +226,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -273,13 +282,13 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -345,12 +354,6 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, "node_modules/@emotion/cache": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", @@ -479,9 +482,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], @@ -496,9 +499,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], @@ -513,9 +516,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], @@ -530,9 +533,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], @@ -547,9 +550,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], @@ -564,9 +567,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], @@ -581,9 +584,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], @@ -598,9 +601,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], @@ -615,9 +618,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], @@ -632,9 +635,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], @@ -649,9 +652,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], @@ -666,9 +669,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], @@ -683,9 +686,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], @@ -700,9 +703,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], @@ -717,9 +720,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], @@ -734,9 +737,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], @@ -751,9 +754,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], @@ -768,9 +771,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ "arm64" ], @@ -785,9 +788,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], @@ -802,9 +805,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "cpu": [ "arm64" ], @@ -819,9 +822,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], @@ -836,9 +839,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], @@ -853,9 +856,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], @@ -870,9 +873,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], @@ -887,9 +890,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], @@ -1191,9 +1194,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.2.tgz", - "integrity": "sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz", + "integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1201,12 +1204,12 @@ } }, "node_modules/@mui/icons-material": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.2.tgz", - "integrity": "sha512-Bo57PFLOqXOqPNrXjd8AhzH5s6TCsNUQbvnQ0VKZ8D+lIlteqKnrk/O1luMJUc/BXONK7BfIdTdc7qOnXYbMdw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.0.tgz", + "integrity": "sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0" + "@babel/runtime": "^7.27.1" }, "engines": { "node": ">=14.0.0" @@ -1216,7 +1219,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^7.0.2", + "@mui/material": "^7.1.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1227,16 +1230,16 @@ } }, "node_modules/@mui/material": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.2.tgz", - "integrity": "sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz", + "integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0", - "@mui/core-downloads-tracker": "^7.0.2", - "@mui/system": "^7.0.2", - "@mui/types": "^7.4.1", - "@mui/utils": "^7.0.2", + "@babel/runtime": "^7.27.1", + "@mui/core-downloads-tracker": "^7.1.0", + "@mui/system": "^7.1.0", + "@mui/types": "^7.4.2", + "@mui/utils": "^7.1.0", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -1255,7 +1258,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.0.2", + "@mui/material-pigment-css": "^7.1.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1276,13 +1279,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.2.tgz", - "integrity": "sha512-6lt8heDC9wN8YaRqEdhqnm0cFCv08AMf4IlttFvOVn7ZdKd81PNpD/rEtPGLLwQAFyyKSxBG4/2XCgpbcdNKiA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", + "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0", - "@mui/utils": "^7.0.2", + "@babel/runtime": "^7.27.1", + "@mui/utils": "^7.1.0", "prop-types": "^15.8.1" }, "engines": { @@ -1303,12 +1306,12 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.2.tgz", - "integrity": "sha512-11Bt4YdHGlh7sB8P75S9mRCUxTlgv7HGbr0UKz6m6Z9KLeiw1Bm9y/t3iqLLVMvSHYB6zL8X8X+LmfTE++gyBw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", + "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0", + "@babel/runtime": "^7.27.1", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", @@ -1337,16 +1340,16 @@ } }, "node_modules/@mui/system": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.2.tgz", - "integrity": "sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz", + "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0", - "@mui/private-theming": "^7.0.2", - "@mui/styled-engine": "^7.0.2", - "@mui/types": "^7.4.1", - "@mui/utils": "^7.0.2", + "@babel/runtime": "^7.27.1", + "@mui/private-theming": "^7.1.0", + "@mui/styled-engine": "^7.1.0", + "@mui/types": "^7.4.2", + "@mui/utils": "^7.1.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1377,12 +1380,12 @@ } }, "node_modules/@mui/types": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz", - "integrity": "sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", + "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0" + "@babel/runtime": "^7.27.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1394,13 +1397,13 @@ } }, "node_modules/@mui/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.0", - "@mui/types": "^7.4.1", + "@babel/runtime": "^7.27.1", + "@mui/types": "^7.4.2", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1472,9 +1475,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", "cpu": [ "arm" ], @@ -1486,9 +1489,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", "cpu": [ "arm64" ], @@ -1500,9 +1503,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", "cpu": [ "arm64" ], @@ -1514,9 +1517,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", "cpu": [ "x64" ], @@ -1528,9 +1531,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", "cpu": [ "arm64" ], @@ -1542,9 +1545,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", "cpu": [ "x64" ], @@ -1556,9 +1559,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", "cpu": [ "arm" ], @@ -1570,9 +1573,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", "cpu": [ "arm" ], @@ -1584,9 +1587,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", "cpu": [ "arm64" ], @@ -1598,9 +1601,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", "cpu": [ "arm64" ], @@ -1612,9 +1615,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", "cpu": [ "loong64" ], @@ -1626,9 +1629,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", "cpu": [ "ppc64" ], @@ -1640,9 +1643,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", "cpu": [ "riscv64" ], @@ -1654,9 +1657,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", "cpu": [ "riscv64" ], @@ -1668,9 +1671,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", "cpu": [ "s390x" ], @@ -1682,9 +1685,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", "cpu": [ "x64" ], @@ -1696,9 +1699,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", "cpu": [ "x64" ], @@ -1710,9 +1713,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", "cpu": [ "arm64" ], @@ -1724,9 +1727,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", "cpu": [ "ia32" ], @@ -1738,9 +1741,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", "cpu": [ "x64" ], @@ -1810,6 +1813,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jwt-decode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-2.2.1.tgz", + "integrity": "sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1823,9 +1833,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", - "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", + "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1851,21 +1861,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", - "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", + "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/type-utils": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1881,16 +1891,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", - "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", + "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "debug": "^4.3.4" }, "engines": { @@ -1906,14 +1916,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", - "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1924,16 +1934,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", - "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", + "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/utils": "8.32.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1948,9 +1958,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", - "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", "dev": true, "license": "MIT", "engines": { @@ -1962,20 +1972,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", - "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2028,16 +2038,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", - "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2052,13 +2062,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", - "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", + "@typescript-eslint/types": "8.32.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2442,10 +2452,9 @@ } }, "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/cookie": { @@ -2603,9 +2612,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.149", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.149.tgz", - "integrity": "sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==", + "version": "1.5.150", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz", + "integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==", "dev": true, "license": "ISC" }, @@ -2674,9 +2683,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2687,31 +2696,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { @@ -3642,6 +3651,15 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4358,9 +4376,9 @@ } }, "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dev": true, "license": "MIT", "dependencies": { @@ -4374,26 +4392,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" } }, @@ -4836,15 +4854,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", - "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", + "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.31.1", - "@typescript-eslint/parser": "8.31.1", - "@typescript-eslint/utils": "8.31.1" + "@typescript-eslint/eslint-plugin": "8.32.0", + "@typescript-eslint/parser": "8.32.0", + "@typescript-eslint/utils": "8.32.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/Frontend/package.json b/Frontend/package.json index d5a5000..6a36d47 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -15,12 +15,14 @@ "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", "axios": "^1.9.0", + "jwt-decode": "^4.0.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.5.3" }, "devDependencies": { "@eslint/js": "^9.25.0", + "@types/jwt-decode": "^2.2.1", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", diff --git a/Frontend/src/components/ChangePasswordModal.tsx b/Frontend/src/components/ChangePasswordModal.tsx new file mode 100644 index 0000000..19ff46b --- /dev/null +++ b/Frontend/src/components/ChangePasswordModal.tsx @@ -0,0 +1,205 @@ +import React, { useState, useEffect } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import authService from '../services/authService'; +import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto'; +import axios from 'axios'; + +import { Modal, Box, Typography, TextField, Button, Alert, CircularProgress, Backdrop } from '@mui/material'; + +const style = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +}; + +interface ChangePasswordModalProps { + open: boolean; + onClose: (success: boolean) => void; + isFirstLogin?: boolean; +} + +const ChangePasswordModal: React.FC = ({ open, onClose, isFirstLogin }) => { + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmNewPassword, setConfirmNewPassword] = useState(''); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [loading, setLoading] = useState(false); + // Ya no necesitamos passwordChangeCompleted ni contextIsFirstLogin aquí + const { user, logout } = useAuth(); + + useEffect(() => { + if (open) { + setCurrentPassword(''); + setNewPassword(''); + setConfirmNewPassword(''); + setError(null); + setSuccess(null); + setLoading(false); // Asegurarse de resetear loading también + } + }, [open]); + + // Esta función se llama al hacer clic en el botón Cancelar + const handleCancelClick = () => { + onClose(false); // Notifica al padre (MainLayout) que se canceló + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setError(null); + setSuccess(null); + + if (newPassword !== confirmNewPassword) { + setError('La nueva contraseña y la confirmación no coinciden.'); + return; + } + if (newPassword.length < 6) { + setError('La nueva contraseña debe tener al menos 6 caracteres.'); + return; + } + if (user && user.username === newPassword) { + setError('La nueva contraseña no puede ser igual al nombre de usuario.'); + return; + } + + setLoading(true); + const changePasswordData: ChangePasswordRequestDto = { + currentPassword, + newPassword, + confirmNewPassword, + }; + + try { + await authService.changePassword(changePasswordData); + setSuccess('Contraseña cambiada exitosamente.'); + setTimeout(() => { + onClose(true); // Notifica al padre (MainLayout) que fue exitoso + }, 1500); + } catch (err: any) { + console.error("Change password error:", err); + let errorMessage = 'Ocurrió un error inesperado al cambiar la contraseña.'; + if (axios.isAxiosError(err) && err.response) { + errorMessage = err.response.data?.message || errorMessage; + if (err.response.status === 401) { + logout(); // Desloguear si el token es inválido + onClose(false); // Notificar cierre sin éxito + } + } + setError(errorMessage); + setLoading(false); // Asegurarse de quitar loading en caso de error + } + // No poner setLoading(false) en el finally si quieres que el botón siga deshabilitado durante el success + }; + + return ( + { // onClose del Modal de MUI (para backdrop y Escape) + if (reason === "backdropClick" && isFirstLogin) { + return; // No permitir cerrar con backdrop si es el primer login + } + onClose(false); // Llamar a la prop onClose (que va a handleModalClose en MainLayout) + }} + disableEscapeKeyDown={isFirstLogin} // Deshabilitar Escape si es primer login + aria-labelledby="change-password-modal-title" + aria-describedby="change-password-modal-description" + closeAfterTransition + slots={{ backdrop: Backdrop }} + slotProps={{ + backdrop: { + timeout: 500, + sx: { backdropFilter: 'blur(3px)' } + }, + }} + > + + + Cambiar Contraseña + + {isFirstLogin && ( + + Por seguridad, debes cambiar tu contraseña inicial. + + )} + + {/* ... TextFields ... */} + setCurrentPassword(e.target.value)} + disabled={loading || !!success} + autoFocus + /> + setNewPassword(e.target.value)} + disabled={loading || !!success} + /> + setConfirmNewPassword(e.target.value)} + disabled={loading || !!success} + error={newPassword !== confirmNewPassword && confirmNewPassword !== ''} + helperText={newPassword !== confirmNewPassword && confirmNewPassword !== '' ? 'Las contraseñas no coinciden' : ''} + /> + + {error && ( + + {error} + + )} + {success && ( + + {success} + + )} + + {/* Un solo grupo de botones */} + + {/* El botón de cancelar llama a handleCancelClick */} + {/* Se podría ocultar si isFirstLogin es true y no queremos que el usuario cancele */} + {/* {!isFirstLogin && ( */} + + {/* )} */} + + + + + + ); +}; + +export default ChangePasswordModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/TipoPagoFormModal.tsx b/Frontend/src/components/Modals/TipoPagoFormModal.tsx new file mode 100644 index 0000000..9e69f04 --- /dev/null +++ b/Frontend/src/components/Modals/TipoPagoFormModal.tsx @@ -0,0 +1,129 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { TipoPago } from '../../models/Entities/TipoPago'; +import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface TipoPagoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateTipoPagoDto | (CreateTipoPagoDto & { idTipoPago: number })) => Promise; // Puede ser para crear o actualizar + initialData?: TipoPago | null; // Datos para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const TipoPagoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [detalle, setDetalle] = useState(''); + const [loading, setLoading] = useState(false); + const [localError, setLocalError] = useState(null); + + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombre(initialData?.nombre || ''); + setDetalle(initialData?.detalle || ''); + setLocalError(null); // Limpiar errores locales al abrir + clearErrorMessage(); // Limpiar errores del padre + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = () => { + if (localError) setLocalError(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalError(null); + clearErrorMessage(); + + if (!nombre.trim()) { + setLocalError('El nombre es obligatorio.'); + return; + } + + setLoading(true); + try { + const dataToSubmit: CreateTipoPagoDto = { nombre, detalle: detalle || undefined }; + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, idTipoPago: initialData.idTipoPago }); + } else { + await onSubmit(dataToSubmit); + } + onClose(); // Cerrar modal en éxito + } catch (error: any) { + // El error de la API ya se debería manejar en el componente padre + // y pasarse a través de 'errorMessage', pero podemos loggear aquí si es un error inesperado + console.error("Error en submit de TipoPagoFormModal:", error); + // No seteamos localError aquí si el padre maneja 'errorMessage' + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Tipo de Pago' : 'Agregar Nuevo Tipo de Pago'} + + + { setNombre(e.target.value); handleInputChange(); }} + margin="normal" + error={!!localError && nombre.trim() === ''} + helperText={localError && nombre.trim() === '' ? localError : ''} + disabled={loading} + /> + { setDetalle(e.target.value); handleInputChange();}} + margin="normal" + multiline + rows={3} + disabled={loading} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default TipoPagoFormModal; \ No newline at end of file diff --git a/Frontend/src/contexts/AuthContext.tsx b/Frontend/src/contexts/AuthContext.tsx index ca1730b..697a902 100644 --- a/Frontend/src/contexts/AuthContext.tsx +++ b/Frontend/src/contexts/AuthContext.tsx @@ -1,13 +1,41 @@ -import React, { createContext, useState, useContext, useEffect } from 'react'; -import type { ReactNode } from 'react'; // Importar como tipo +import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react'; import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; +import { jwtDecode } from 'jwt-decode'; + +// Interfaz para los datos del usuario que guardaremos en el contexto +export interface UserContextData { + userId: number; + username: string; + nombreCompleto: string; + esSuperAdmin: boolean; + debeCambiarClave: boolean; + idPerfil: number; + permissions: string[]; // Guardamos los codAcc +} + +// Interfaz para el payload decodificado del JWT +interface DecodedJwtPayload { + sub: string; // User ID (viene como string) + name: string; // Username + given_name?: string; // Nombre (estándar, pero verifica tu token) + family_name?: string; // Apellido (estándar, pero verifica tu token) + role: string | string[]; // Puede ser uno o varios roles + idPerfil: string; // (viene como string) + debeCambiarClave: string; // (viene como string "True" o "False") + permission?: string | string[]; // Nuestros claims de permiso (codAcc) + [key: string]: any; // Para otros claims +} interface AuthContextType { isAuthenticated: boolean; - user: LoginResponseDto | null; + user: UserContextData | null; // Usar el tipo extendido token: string | null; - isLoading: boolean; // Para saber si aún está verificando el token inicial - login: (userData: LoginResponseDto) => void; + isLoading: boolean; + showForcedPasswordChangeModal: boolean; + isPasswordChangeForced: boolean; + setShowForcedPasswordChangeModal: (show: boolean) => void; + passwordChangeCompleted: () => void; + login: (apiLoginResponse: LoginResponseDto) => void; // Recibe el DTO de la API logout: () => void; } @@ -15,37 +43,68 @@ const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); - const [user, setUser] = useState(null); + const [user, setUser] = useState(null); const [token, setToken] = useState(null); - const [isLoading, setIsLoading] = useState(true); // Empieza cargando + const [isLoading, setIsLoading] = useState(true); + const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState(false); + const [isPasswordChangeForced, setIsPasswordChangeForced] = useState(false); - // Efecto para verificar token al cargar la app - useEffect(() => { - const storedToken = localStorage.getItem('authToken'); - const storedUser = localStorage.getItem('authUser'); // Guardamos el usuario también + const processTokenAndSetUser = (jwtToken: string) => { + try { + const decodedToken = jwtDecode(jwtToken); - if (storedToken && storedUser) { - try { - // Aquí podrías añadir lógica para validar si el token aún es válido (ej: decodificarlo) - // Por ahora, simplemente asumimos que si está, es válido. - const parsedUser: LoginResponseDto = JSON.parse(storedUser); - setToken(storedToken); - setUser(parsedUser); - setIsAuthenticated(true); - } catch (error) { - console.error("Error parsing stored user data", error); - logout(); // Limpia si hay error al parsear + // Verificar expiración (opcional, pero buena práctica aquí también) + const currentTime = Date.now() / 1000; + if (decodedToken.exp && decodedToken.exp < currentTime) { + console.warn("Token expirado al procesar."); + logout(); // Llama a logout que limpia todo + return; } + + let permissions: string[] = []; + if (decodedToken.permission) { + permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission]; + } + + const userForContext: UserContextData = { + userId: parseInt(decodedToken.sub, 10), + username: decodedToken.name, + nombreCompleto: `${decodedToken.given_name || ''} ${decodedToken.family_name || ''}`.trim(), + esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")), + debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true', + idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0, + permissions: permissions, + }; + + setToken(jwtToken); + setUser(userForContext); + setIsAuthenticated(true); + localStorage.setItem('authToken', jwtToken); + localStorage.setItem('authUser', JSON.stringify(userForContext)); // Guardar el usuario procesado + + // Lógica para el modal de cambio de clave + if (userForContext.debeCambiarClave) { + setShowForcedPasswordChangeModal(true); + setIsPasswordChangeForced(true); + } + + } catch (error) { + console.error("Error al decodificar o procesar token:", error); + logout(); // Limpiar estado si el token es inválido } - setIsLoading(false); // Termina la carga inicial + }; + + useEffect(() => { + setIsLoading(true); + const storedToken = localStorage.getItem('authToken'); + if (storedToken) { + processTokenAndSetUser(storedToken); + } + setIsLoading(false); }, []); - const login = (userData: LoginResponseDto) => { - localStorage.setItem('authToken', userData.Token); - localStorage.setItem('authUser', JSON.stringify(userData)); // Guardar datos de usuario - setToken(userData.Token); - setUser(userData); - setIsAuthenticated(true); + const login = (apiLoginResponse: LoginResponseDto) => { + processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido }; const logout = () => { @@ -54,16 +113,36 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setToken(null); setUser(null); setIsAuthenticated(false); + setShowForcedPasswordChangeModal(false); + setIsPasswordChangeForced(false); + }; + + const passwordChangeCompleted = () => { + setShowForcedPasswordChangeModal(false); + setIsPasswordChangeForced(false); + // Importante: Si el cambio de clave afecta el claim "debeCambiarClave" en el token, + // idealmente el backend debería devolver un *nuevo token* después del cambio de clave. + // Si no lo hace, el token actual aún dirá que debe cambiar clave. + // Una solución simple es actualizar el estado local del usuario: + if (user) { + const updatedUser = { ...user, debeCambiarClave: false }; + setUser(updatedUser); + localStorage.setItem('authUser', JSON.stringify(updatedUser)); + } }; return ( - + {children} ); }; -// Hook personalizado para usar el contexto fácilmente export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (context === undefined) { diff --git a/Frontend/src/hooks/usePermissions.ts b/Frontend/src/hooks/usePermissions.ts new file mode 100644 index 0000000..0d6f5e3 --- /dev/null +++ b/Frontend/src/hooks/usePermissions.ts @@ -0,0 +1,26 @@ +// src/hooks/usePermissions.ts +import { useAuth } from '../contexts/AuthContext'; + +export const usePermissions = () => { + const { user } = useAuth(); // user aquí es de tipo UserContextData | null + + const tienePermiso = (codigoPermisoRequerido: string): boolean => { + if (!user) { // Si no hay usuario logueado + return false; + } + if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos + return true; + } + // Verificar si la lista de permisos del usuario incluye el código requerido + return user.permissions?.includes(codigoPermisoRequerido) ?? false; + }; + + // También puede exportar el objeto user completo si se necesita en otros lugares + // o propiedades específicas como idPerfil, esSuperAdmin. + return { + tienePermiso, + isSuperAdmin: user?.esSuperAdmin ?? false, + idPerfil: user?.idPerfil ?? 0, + currentUser: user + }; +}; \ No newline at end of file diff --git a/Frontend/src/layouts/MainLayout.tsx b/Frontend/src/layouts/MainLayout.tsx index db7c80b..1e36878 100644 --- a/Frontend/src/layouts/MainLayout.tsx +++ b/Frontend/src/layouts/MainLayout.tsx @@ -1,46 +1,152 @@ -import React from 'react'; -import type { ReactNode } from 'react'; // Importar como tipo -import { Box, AppBar, Toolbar, Typography, Button } from '@mui/material'; +import React, { type ReactNode, useState, useEffect } from 'react'; +import { Box, AppBar, Toolbar, Typography, Button, Tabs, Tab, Paper } from '@mui/material'; import { useAuth } from '../contexts/AuthContext'; +import ChangePasswordModal from '../components/ChangePasswordModal'; +import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual interface MainLayoutProps { - children: ReactNode; // Para renderizar las páginas hijas + children: ReactNode; // Esto será el que renderiza las páginas del módulo } +// Definir los módulos y sus rutas base +const modules = [ + { label: 'Inicio', path: '/' }, + { label: 'Distribución', path: '/distribucion' }, // Asumiremos rutas base como /distribucion, /contables, etc. + { label: 'Contables', path: '/contables' }, + { label: 'Impresión', path: '/impresion' }, + { label: 'Reportes', path: '/reportes' }, + { label: 'Radios', path: '/radios' }, + { label: 'Usuarios', path: '/usuarios' }, +]; + const MainLayout: React.FC = ({ children }) => { - const { user, logout } = useAuth(); + const { + user, + logout, + showForcedPasswordChangeModal, + isPasswordChangeForced, + passwordChangeCompleted, + setShowForcedPasswordChangeModal, + isAuthenticated + } = useAuth(); + + const navigate = useNavigate(); + const location = useLocation(); // Para obtener la ruta actual + + // Estado para el tab seleccionado + const [selectedTab, setSelectedTab] = useState(false); + + // Efecto para sincronizar el tab seleccionado con la ruta actual + useEffect(() => { + const currentModulePath = modules.findIndex(module => + location.pathname === module.path || (module.path !== '/' && location.pathname.startsWith(module.path + '/')) + ); + if (currentModulePath !== -1) { + setSelectedTab(currentModulePath); + } else if (location.pathname === '/') { + setSelectedTab(0); // Seleccionar "Inicio" si es la raíz + } else { + setSelectedTab(false); // Ningún tab coincide (podría ser una sub-ruta no principal) + } + }, [location.pathname]); + + const handleModalClose = (passwordChangedSuccessfully: boolean) => { + // ... (lógica de handleModalClose existente) ... + if (passwordChangedSuccessfully) { + passwordChangeCompleted(); + } else { + if (isPasswordChangeForced) { + logout(); + } else { + setShowForcedPasswordChangeModal(false); + } + } + }; + + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedTab(newValue); + navigate(modules[newValue].path); // Navegar a la ruta base del módulo + }; + + // Si el modal de cambio de clave forzado está activo, no mostramos la navegación principal aún. + // El modal se superpone. + if (showForcedPasswordChangeModal && isPasswordChangeForced) { + return ( + + + {/* Podrías querer un fondo o layout mínimo aquí si el modal no es pantalla completa */} + + ); + } + return ( - Gestión Integral + Sistema de Gestión - El Día - {user && Hola, {user.Username} } + {user && Hola, {user.nombreCompleto}} + {isAuthenticated && !isPasswordChangeForced && ( + + )} - {/* Aquí iría el MaterialTabControl o similar para la navegación principal */} + {/* Navegación Principal por Módulos */} + {/* Usamos Paper para un fondo consistente para los Tabs */} + + {modules.map((module) => ( + + ))} + + + + {/* Contenido del Módulo (renderizado por en AppRoutes) */} theme.palette.background.default, + // overflowY: 'auto' // Si el contenido del módulo es muy largo }} > - {/* El contenido de la página actual se renderizará aquí */} {children} - {/* Aquí podría ir un Footer o StatusStrip */} - + + - {/* Replicar info del StatusStrip original */} - Usuario: {user?.Username} | Acceso: {user?.EsSuperAdmin ? 'Super Admin' : 'Perfil...'} | Versión: {/** Obtener versión **/} + Usuario: {user?.username} | Acceso: {user?.esSuperAdmin ? 'Super Admin' : `Perfil ID ${user?.userId}`} | Versión: {/* TODO: Obtener versión */} + + {/* Modal para cambio de clave opcional (no forzado) */} + {/* Si showForcedPasswordChangeModal es true pero isPasswordChangeForced es false, + se mostrará aquí también. */} + ); }; diff --git a/Frontend/src/models/Entities/TipoPago.ts b/Frontend/src/models/Entities/TipoPago.ts new file mode 100644 index 0000000..56c4593 --- /dev/null +++ b/Frontend/src/models/Entities/TipoPago.ts @@ -0,0 +1,5 @@ +export interface TipoPago { + idTipoPago: number; + nombre: string; + detalle?: string; // El detalle es opcional +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/ChangePasswordRequestDto.ts b/Frontend/src/models/dtos/ChangePasswordRequestDto.ts new file mode 100644 index 0000000..652113f --- /dev/null +++ b/Frontend/src/models/dtos/ChangePasswordRequestDto.ts @@ -0,0 +1,6 @@ +// src/models/dtos/ChangePasswordRequestDto.ts +export interface ChangePasswordRequestDto { + currentPassword: string; + newPassword: string; + confirmNewPassword: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/LoginResponseDto.ts b/Frontend/src/models/dtos/LoginResponseDto.ts index fc9e7a5..dbfd953 100644 --- a/Frontend/src/models/dtos/LoginResponseDto.ts +++ b/Frontend/src/models/dtos/LoginResponseDto.ts @@ -1,10 +1,10 @@ // src/models/dtos/LoginResponseDto.ts export interface LoginResponseDto { - Token: string; - UserId: number; - Username: string; - NombreCompleto: string; - EsSuperAdmin: boolean; - DebeCambiarClave: boolean; + token: string; + userId: number; + username: string; + nombreCompleto: string; + esSuperAdmin: boolean; + debeCambiarClave: boolean; // Añade otros campos si los definiste en el DTO C# } \ No newline at end of file diff --git a/Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts b/Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts new file mode 100644 index 0000000..b4c2784 --- /dev/null +++ b/Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts @@ -0,0 +1,4 @@ +export interface CreateTipoPagoDto { + nombre: string; + detalle?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts b/Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts new file mode 100644 index 0000000..b86e1a2 --- /dev/null +++ b/Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts @@ -0,0 +1,4 @@ +export interface UpdateTipoPagoDto { + nombre: string; + detalle?: string; +} \ No newline at end of file diff --git a/Frontend/src/pages/ChangePasswordPage.tsx b/Frontend/src/pages/ChangePasswordPage.tsx deleted file mode 100644 index 28dcc5c..0000000 --- a/Frontend/src/pages/ChangePasswordPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Typography, Container } from '@mui/material'; -// import { useLocation } from 'react-router-dom'; // Para obtener el estado 'firstLogin' - -const ChangePasswordPage: React.FC = () => { - // const location = useLocation(); - // const isFirstLogin = location.state?.firstLogin; - - return ( - - - Cambiar Contraseña - - {/* {isFirstLogin && Debes cambiar tu contraseña inicial.} */} - {/* Aquí irá el formulario de cambio de contraseña */} - - Formulario de cambio de contraseña irá aquí... - - - ); -}; - -export default ChangePasswordPage; \ No newline at end of file diff --git a/Frontend/src/pages/ChangePasswordPagePlaceholder.tsx b/Frontend/src/pages/ChangePasswordPagePlaceholder.tsx new file mode 100644 index 0000000..0e48a65 --- /dev/null +++ b/Frontend/src/pages/ChangePasswordPagePlaceholder.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Typography, Container, Button } from '@mui/material'; +import { useAuth } from '../contexts/AuthContext'; + +const ChangePasswordPagePlaceholder: React.FC = () => { + const { setShowForcedPasswordChangeModal } = useAuth(); + return ( + + + Cambiar Contraseña (Página) + + + La funcionalidad de cambio de contraseña ahora se maneja principalmente a través de un modal. + + + + ); +}; + +export default ChangePasswordPagePlaceholder; \ No newline at end of file diff --git a/Frontend/src/pages/Contables/ContablesIndexPage.tsx b/Frontend/src/pages/Contables/ContablesIndexPage.tsx new file mode 100644 index 0000000..c4b5a73 --- /dev/null +++ b/Frontend/src/pages/Contables/ContablesIndexPage.tsx @@ -0,0 +1,70 @@ +// src/pages/contables/ContablesIndexPage.tsx +import React, { useState, useEffect } from 'react'; +import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; + +// Define las sub-pestañas del módulo Contables +const contablesSubModules = [ + { label: 'Tipos de Pago', path: 'tipos-pago' }, // Se convertirá en /contables/tipos-pago + // { label: 'Pagos', path: 'pagos' }, // Ejemplo de otra sub-pestaña futura + // { label: 'Créditos/Débitos', path: 'creditos-debitos' }, +]; + +const ContablesIndexPage: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [selectedSubTab, setSelectedSubTab] = useState(false); + + useEffect(() => { + const currentBasePath = '/contables'; + const subPath = location.pathname.startsWith(currentBasePath + '/') + ? location.pathname.substring(currentBasePath.length + 1) + : (location.pathname === currentBasePath ? contablesSubModules[0]?.path : undefined); + + const activeTabIndex = contablesSubModules.findIndex( + (subModule) => subModule.path === subPath + ); + + if (activeTabIndex !== -1) { + setSelectedSubTab(activeTabIndex); + } else { + if (location.pathname === currentBasePath && contablesSubModules.length > 0) { + navigate(contablesSubModules[0].path, { replace: true }); + setSelectedSubTab(0); + } else { + setSelectedSubTab(false); + } + } + }, [location.pathname, navigate]); + + const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedSubTab(newValue); + navigate(contablesSubModules[newValue].path); + }; + + return ( + + Módulo Contable + + + {contablesSubModules.map((subModule) => ( + + ))} + + + + {/* Aquí se renderizarán GestionarTiposPagoPage, etc. */} + + + ); +}; + +export default ContablesIndexPage; \ No newline at end of file diff --git a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx new file mode 100644 index 0000000..8ff8417 --- /dev/null +++ b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx @@ -0,0 +1,242 @@ +// src/pages/configuracion/GestionarTiposPagoPage.tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import tipoPagoService from '../../services/tipoPagoService'; +import type { TipoPago } from '../../models/Entities/TipoPago'; +import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; +import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto'; +import TipoPagoFormModal from '../../components/Modals/TipoPagoFormModal'; +import axios from 'axios'; +import { usePermissions } from '../../hooks/usePermissions'; + +const GestionarTiposPagoPage: React.FC = () => { + const [tiposPago, setTiposPago] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingTipoPago, setEditingTipoPago] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + // Para el menú contextual de cada fila + const [anchorEl, setAnchorEl] = useState(null); + const [selectedTipoPagoRow, setSelectedTipoPagoRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); // Obtener también isSuperAdmin + + const puedeCrear = isSuperAdmin || tienePermiso("CT002"); + const puedeModificar = isSuperAdmin || tienePermiso("CT003"); + const puedeEliminar = isSuperAdmin || tienePermiso("CT004"); + + + const cargarTiposPago = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await tipoPagoService.getAllTiposPago(filtroNombre); + setTiposPago(data); + } catch (err) { + console.error(err); + setError('Error al cargar los tipos de pago.'); + } finally { + setLoading(false); + } + }, [filtroNombre]); + + useEffect(() => { + cargarTiposPago(); + }, [cargarTiposPago]); + + const handleOpenModal = (tipoPago?: TipoPago) => { + setEditingTipoPago(tipoPago || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingTipoPago(null); + }; + + const handleSubmitModal = async (data: CreateTipoPagoDto | UpdateTipoPagoDto) => { + setApiErrorMessage(null); // Limpiar error previo + try { + if (editingTipoPago && 'idTipoPago' in data) { // Es Update + await tipoPagoService.updateTipoPago(editingTipoPago.idTipoPago, data as UpdateTipoPagoDto); + } else { // Es Create + await tipoPagoService.createTipoPago(data as CreateTipoPagoDto); + } + cargarTiposPago(); // Recargar lista + // onClose se llama desde el modal en caso de éxito + } catch (err: any) { + console.error("Error en submit modal (padre):", err); + if (axios.isAxiosError(err) && err.response) { + setApiErrorMessage(err.response.data?.message || 'Error al guardar.'); + } else { + setApiErrorMessage('Ocurrió un error inesperado al guardar.'); + } + throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm('¿Está seguro de que desea eliminar este tipo de pago?')) { + setApiErrorMessage(null); + try { + await tipoPagoService.deleteTipoPago(id); + cargarTiposPago(); + } catch (err: any) { + console.error(err); + if (axios.isAxiosError(err) && err.response) { + setApiErrorMessage(err.response.data?.message || 'Error al eliminar.'); + } else { + setApiErrorMessage('Ocurrió un error inesperado al eliminar.'); + } + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, tipoPago: TipoPago) => { + setAnchorEl(event.currentTarget); + setSelectedTipoPagoRow(tipoPago); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedTipoPagoRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const displayData = tiposPago.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + return ( + + + Gestionar Tipos de Pago + + + + + setFiltroNombre(e.target.value)} + // sx={{ flexGrow: 1 }} // Opcional, para que ocupe más espacio + /> + {/* El botón de búsqueda se activa al cambiar el texto, pero puedes añadir uno explícito */} + {/* */} + + {puedeCrear && ( + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + + {!loading && !error && ( + + + + + Nombre + Detalle + Acciones + + + + {displayData.length === 0 && !loading ? ( + No se encontraron tipos de pago. + ) : ( + displayData.map((tipo) => ( + + {tipo.nombre} + {tipo.detalle || '-'} + + handleMenuOpen(e, tipo)} + disabled={!puedeModificar && !puedeEliminar} + > + + + + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedTipoPagoRow!); handleMenuClose(); }}> + Modificar + + )} + {puedeEliminar && ( + handleDelete(selectedTipoPagoRow!.idTipoPago)}> + Eliminar + + )} + {/* Si no tiene ningún permiso, el menú podría estar vacío o no mostrarse */} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarTiposPagoPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/CanillasPage.tsx b/Frontend/src/pages/Distribucion/CanillasPage.tsx new file mode 100644 index 0000000..16e3daf --- /dev/null +++ b/Frontend/src/pages/Distribucion/CanillasPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const CanillasPage: React.FC = () => { + return Página de Gestión de Canillas; +}; +export default CanillasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/ControlDevolucionesPage.tsx b/Frontend/src/pages/Distribucion/ControlDevolucionesPage.tsx new file mode 100644 index 0000000..7398bb8 --- /dev/null +++ b/Frontend/src/pages/Distribucion/ControlDevolucionesPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const CtrlDevolucionesPage: React.FC = () => { + return Página de Gestión del Control de Devoluciones; +}; +export default CtrlDevolucionesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx b/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx new file mode 100644 index 0000000..3f77347 --- /dev/null +++ b/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx @@ -0,0 +1,88 @@ +// src/pages/distribucion/DistribucionIndexPage.tsx +import React, { useState, useEffect } from 'react'; +import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; +import { Outlet, useNavigate, useLocation, Link as RouterLink } from 'react-router-dom'; + +// Define las sub-pestañas del módulo Distribución +// El path es relativo a la ruta base del módulo (ej: /distribucion) +const distribucionSubModules = [ + { label: 'E/S Canillas', path: 'es-canillas' }, // Se convertirá en /distribucion/es-canillas + { label: 'Ctrl. Devoluciones', path: 'control-devoluciones' }, + { label: 'E/S Distribuidores', path: 'es-distribuidores' }, + { label: 'Salidas Otros Dest.', path: 'salidas-otros-destinos' }, + { label: 'Canillas', path: 'canillas' }, + { label: 'Distribuidores', path: 'distribuidores' }, + { label: 'Publicaciones', path: 'publicaciones' }, + { label: 'Otros Destinos', path: 'otros-destinos' }, + { label: 'Zonas', path: 'zonas' }, + { label: 'Empresas', path: 'empresas' }, +]; + +const DistribucionIndexPage: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [selectedSubTab, setSelectedSubTab] = useState(false); + + // Sincronizar el sub-tab con la URL actual + useEffect(() => { + // location.pathname será algo como /distribucion/canillas + // Necesitamos extraer la última parte para compararla con los paths de subSubModules + const currentBasePath = '/distribucion'; // Ruta base de este módulo + const subPath = location.pathname.startsWith(currentBasePath + '/') + ? location.pathname.substring(currentBasePath.length + 1) + : (location.pathname === currentBasePath ? distribucionSubModules[0]?.path : undefined); // Si es /distribucion, selecciona el primero + + const activeTabIndex = distribucionSubModules.findIndex( + (subModule) => subModule.path === subPath + ); + + if (activeTabIndex !== -1) { + setSelectedSubTab(activeTabIndex); + } else { + // Si no coincide ninguna sub-ruta, pero estamos en /distribucion, ir al primer tab + if (location.pathname === currentBasePath && distribucionSubModules.length > 0) { + navigate(distribucionSubModules[0].path, { replace: true }); // Navegar a la primera sub-ruta + setSelectedSubTab(0); + } else { + setSelectedSubTab(false); // Ningún sub-tab activo + } + } + }, [location.pathname, navigate]); + + const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedSubTab(newValue); + navigate(distribucionSubModules[newValue].path); // Navega a la sub-ruta (ej: 'canillas') + }; + + return ( + + Módulo de Distribución + + + {distribucionSubModules.map((subModule) => ( + // Usar RouterLink para que el tab se comporte como un enlace y actualice la URL + // La navegación real la manejamos con navigate en handleSubTabChange + // para poder actualizar el estado del tab seleccionado. + // Podríamos usar `component={RouterLink} to={subModule.path}` también, + // pero manejarlo con navigate da más control sobre el estado. + + ))} + + + {/* Padding para el contenido de la sub-pestaña */} + {/* Outlet renderizará el componente de la sub-ruta activa (ej: CanillasPage) */} + + + + ); +}; + +export default DistribucionIndexPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/DistribuidoresPage.tsx b/Frontend/src/pages/Distribucion/DistribuidoresPage.tsx new file mode 100644 index 0000000..706d12f --- /dev/null +++ b/Frontend/src/pages/Distribucion/DistribuidoresPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const DistribuidoresPage: React.FC = () => { + return Página de Gestión de Distribuidores; +}; +export default DistribuidoresPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/ESCanillasPage.tsx b/Frontend/src/pages/Distribucion/ESCanillasPage.tsx new file mode 100644 index 0000000..19b77fd --- /dev/null +++ b/Frontend/src/pages/Distribucion/ESCanillasPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const ESCanillasPage: React.FC = () => { + return Página de Gestión de E/S de Canillas; +}; +export default ESCanillasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx b/Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx new file mode 100644 index 0000000..2a824db --- /dev/null +++ b/Frontend/src/pages/Distribucion/ESDistribuidoresPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const ESDistribuidoresPage: React.FC = () => { + return Página de Gestión de E/S de Distribuidores; +}; +export default ESDistribuidoresPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/EmpresasPage.tsx b/Frontend/src/pages/Distribucion/EmpresasPage.tsx new file mode 100644 index 0000000..ebed34c --- /dev/null +++ b/Frontend/src/pages/Distribucion/EmpresasPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const EmpresasPage: React.FC = () => { + return Página de Gestión de Empresas; +}; +export default EmpresasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx new file mode 100644 index 0000000..61d7ccf --- /dev/null +++ b/Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const OtrosDestinosPage: React.FC = () => { + return Página de Gestión de Otros Destinos; +}; +export default OtrosDestinosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/PublicacionesPage.tsx b/Frontend/src/pages/Distribucion/PublicacionesPage.tsx new file mode 100644 index 0000000..9fc64d3 --- /dev/null +++ b/Frontend/src/pages/Distribucion/PublicacionesPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const PublicacionesPage: React.FC = () => { + return Página de Gestión de Publicaciones; +}; +export default PublicacionesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/SalidasOtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/SalidasOtrosDestinosPage.tsx new file mode 100644 index 0000000..5681e2f --- /dev/null +++ b/Frontend/src/pages/Distribucion/SalidasOtrosDestinosPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const SalidastrosDestinosPage: React.FC = () => { + return Página de Gestión de Salidas a Otros Destinos; +}; +export default SalidastrosDestinosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/ZonasPage.tsx b/Frontend/src/pages/Distribucion/ZonasPage.tsx new file mode 100644 index 0000000..4e64883 --- /dev/null +++ b/Frontend/src/pages/Distribucion/ZonasPage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +const ZonasPage: React.FC = () => { + return Página de Gestión de Zonas; +}; +export default ZonasPage; \ No newline at end of file diff --git a/Frontend/src/pages/LoginPage.tsx b/Frontend/src/pages/LoginPage.tsx index 969fa1c..4b10886 100644 --- a/Frontend/src/pages/LoginPage.tsx +++ b/Frontend/src/pages/LoginPage.tsx @@ -1,13 +1,11 @@ import React, { useState } from 'react'; import axios from 'axios'; // Importar axios -import { useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; -import apiClient from '../services/apiClient'; // Nuestro cliente axios import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; // Usar type -import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; // Usar type // Importaciones de Material UI import { Container, TextField, Button, Typography, Box, Alert } from '@mui/material'; +import authService from '../services/authService'; const LoginPage: React.FC = () => { const [username, setUsername] = useState(''); @@ -15,7 +13,6 @@ const LoginPage: React.FC = () => { const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const { login } = useAuth(); - const navigate = useNavigate(); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -25,25 +22,17 @@ const LoginPage: React.FC = () => { const loginData: LoginRequestDto = { Username: username, Password: password }; try { - const response = await apiClient.post('/auth/login', loginData); - login(response.data); // Guardar token y estado de usuario en el contexto - - // TODO: Verificar si response.data.DebeCambiarClave es true y redirigir - // a '/change-password' si es necesario. - // if (response.data.DebeCambiarClave) { - // navigate('/change-password', { state: { firstLogin: true } }); // Pasar estado si es necesario - // } else { - navigate('/'); // Redirigir a la página principal - // } - + const response = await authService.login(loginData); + login(response); } catch (err: any) { console.error("Login error:", err); if (axios.isAxiosError(err) && err.response) { - // Intenta obtener el mensaje de error de la API, si no, usa uno genérico setError(err.response.data?.message || 'Error al iniciar sesión. Verifique sus credenciales.'); } else { setError('Ocurrió un error inesperado.'); } + // Importante: NO llamar a navigate('/') aquí en el catch, + // porque el estado isAuthenticated no habrá cambiado a true } finally { setLoading(false); } diff --git a/Frontend/src/routes/AppRoutes.tsx b/Frontend/src/routes/AppRoutes.tsx index 68716be..4f5d4e2 100644 --- a/Frontend/src/routes/AppRoutes.tsx +++ b/Frontend/src/routes/AppRoutes.tsx @@ -1,63 +1,125 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +// src/routes/AppRoutes.tsx +import React, { type JSX } from 'react'; +import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import LoginPage from '../pages/LoginPage'; -import HomePage from '../pages/HomePage'; // Crearemos esta página simple +import HomePage from '../pages/HomePage'; import { useAuth } from '../contexts/AuthContext'; -import ChangePasswordPage from '../pages/ChangePasswordPage'; // Crearemos esta -import MainLayout from '../layouts/MainLayout'; // Crearemos este +import MainLayout from '../layouts/MainLayout'; +import { Typography } from '@mui/material'; -// Componente para proteger rutas +// Distribución +import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage'; +import ESCanillasPage from '../pages/Distribucion/ESCanillasPage'; +import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage'; +import ESDistribuidoresPage from '../pages/Distribucion/ESDistribuidoresPage'; +import SalidasOtrosDestinosPage from '../pages/Distribucion/SalidasOtrosDestinosPage'; +import CanillasPage from '../pages/Distribucion/CanillasPage'; +import DistribuidoresPage from '../pages/Distribucion/DistribuidoresPage'; +import PublicacionesPage from '../pages/Distribucion/PublicacionesPage'; +import OtrosDestinosPage from '../pages/Distribucion/OtrosDestinosPage'; +import ZonasPage from '../pages/Distribucion/ZonasPage'; +import EmpresasPage from '../pages/Distribucion/EmpresasPage'; + +// Contables +import ContablesIndexPage from '../pages/Contables/ContablesIndexPage'; +import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage'; // Asumiendo que lo moviste aquí + +// --- ProtectedRoute y PublicRoute SIN CAMBIOS --- const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { - const { isAuthenticated, isLoading } = useAuth(); - - if (isLoading) { - // Muestra algo mientras verifica el token (ej: un spinner) - return
Cargando...
; - } - - return isAuthenticated ? children : ; + const { isAuthenticated, isLoading } = useAuth(); + // console.log("ProtectedRoute Check:", { path: window.location.pathname, isAuthenticated, isLoading }); + if (isLoading) return null; + if (!isAuthenticated) { + // console.log("ProtectedRoute: Not authenticated, redirecting to /login"); + return ; + } + // console.log("ProtectedRoute: Authenticated, rendering children"); + return children; }; - -// Componente para rutas públicas (redirige si ya está logueado) const PublicRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { const { isAuthenticated, isLoading } = useAuth(); - - if (isLoading) { - return
Cargando...
; + // console.log("PublicRoute Check:", { path: window.location.pathname, isAuthenticated, isLoading }); + if (isLoading) return null; + if (isAuthenticated) { + // console.log("PublicRoute: Authenticated, redirecting to /"); + return ; } - - return !isAuthenticated ? children : ; + // console.log("PublicRoute: Not authenticated, rendering children"); + return children; }; +// --- Fin Protected/Public --- +const MainLayoutWrapper: React.FC = () => ( + + + +); + +// Placeholder simple +const PlaceholderPage: React.FC<{ moduleName: string }> = ({ moduleName }) => ( + Página Principal del Módulo: {moduleName} +); const AppRoutes = () => { - return ( - - - {/* Rutas Públicas */} - } /> - } /> {/* Asumimos que se accede logueado */} +return ( + + {/* Un solo de nivel superior */} + } /> - {/* Rutas Protegidas dentro del Layout Principal */} - - {/* Layout que tendrá la navegación principal */} - {/* Aquí irán las rutas de los módulos */} - - } /> {/* Página por defecto al loguearse */} - {/* } /> */} - {/* } /> */} - {/* ... otras rutas de módulos ... */} - } /> {/* Redirige rutas no encontradas al home */} - - - - } - /> - - - ); + {/* Rutas Protegidas que usan el MainLayout */} + + + + } + > + {/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */} + } /> {/* Para la ruta exacta "/" */} + + {/* Módulo de Distribución (anidado) */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Módulo Contable (anidado) */} + }> + } /> + } /> + {/* Futuras sub-rutas de contables aquí */} + + + {/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */} + } /> + } /> + } /> + {/* } /> */} + + {/* Ruta catch-all DENTRO del layout protegido */} + } /> + {/* Cierre de la ruta padre "/" */} + + {/* Podrías tener un catch-all global aquí si una ruta no coincide EN ABSOLUTO, + pero el path="*" dentro de la ruta "/" ya debería manejar la mayoría de los casos + después de un login exitoso. + Si un usuario no autenticado intenta una ruta inválida, ProtectedRoute lo manda a /login. + */} + {/* } /> */} + + + +); }; export default AppRoutes; \ No newline at end of file diff --git a/Frontend/src/services/authService.ts b/Frontend/src/services/authService.ts new file mode 100644 index 0000000..be22bd7 --- /dev/null +++ b/Frontend/src/services/authService.ts @@ -0,0 +1,21 @@ +import apiClient from './apiClient'; +import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; +import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; +import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto'; // Importar DTO + +const login = async (credentials: LoginRequestDto): Promise => { + const response = await apiClient.post('/auth/login', credentials); + return response.data; +}; + +const changePassword = async (data: ChangePasswordRequestDto): Promise => { + // No esperamos datos de vuelta, solo éxito (204) o error + await apiClient.post('/auth/change-password', data); +}; + +const authService = { + login, + changePassword, // Exportar la nueva función +}; + +export default authService; \ No newline at end of file diff --git a/Frontend/src/services/tipoPagoService.ts b/Frontend/src/services/tipoPagoService.ts new file mode 100644 index 0000000..28073cf --- /dev/null +++ b/Frontend/src/services/tipoPagoService.ts @@ -0,0 +1,43 @@ +import apiClient from './apiClient'; +import type { TipoPago } from '../models/Entities/TipoPago'; +import type { CreateTipoPagoDto } from '../models/dtos/tiposPago/CreateTipoPagoDto'; +import type { UpdateTipoPagoDto } from '../models/dtos/tiposPago/UpdateTipoPagoDto'; + +const getAllTiposPago = async (nombreFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) { + params.nombre = nombreFilter; + } + const response = await apiClient.get('/tipospago', { params }); + return response.data; +}; + +const getTipoPagoById = async (id: number): Promise => { + const response = await apiClient.get(`/tipospago/${id}`); + return response.data; +}; + +const createTipoPago = async (data: CreateTipoPagoDto): Promise => { + const response = await apiClient.post('/tipospago', data); + return response.data; // La API devuelve el objeto creado (201 Created) +}; + +const updateTipoPago = async (id: number, data: UpdateTipoPagoDto): Promise => { + // PUT no suele devolver contenido en éxito (204 No Content) + await apiClient.put(`/tipospago/${id}`, data); +}; + +const deleteTipoPago = async (id: number): Promise => { + // DELETE no suele devolver contenido en éxito (204 No Content) + await apiClient.delete(`/tipospago/${id}`); +}; + +const tipoPagoService = { + getAllTiposPago, + getTipoPagoById, + createTipoPago, + updateTipoPago, + deleteTipoPago, +}; + +export default tipoPagoService; \ No newline at end of file