From 5ddef72f065231d802c4edbcb8ed94d8f4fa5af3 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Fri, 12 Dec 2025 15:40:34 -0300 Subject: [PATCH] Init Commit --- .env.example | 15 + .gitignore | 90 + .../Controllers/AuthController.cs | 115 + .../Controllers/ConfiguracionController.cs | 316 ++ .../Controllers/OperacionesController.cs | 224 + .../Data/ApplicationDbContext.cs | 66 + .../Data/ApplicationDbContextFactory.cs | 16 + Backend/GestorFacturas.API/Dockerfile | 33 + .../GestorFacturas.API.csproj | 27 + .../GestorFacturas.API.http | 6 + .../20251210145015_InitialCreate.Designer.cs | 172 + .../20251210145015_InitialCreate.cs | 88 + ...1210155728_AgregaAutenticacion.Designer.cs | 207 + .../20251210155728_AgregaAutenticacion.cs | 41 + ...1210164013_UpdateAdminPassword.Designer.cs | 207 + .../20251210164013_UpdateAdminPassword.cs | 32 + ...0251210171018_AddRefreshTokens.Designer.cs | 249 + .../20251210171018_AddRefreshTokens.cs | 50 + ..._AddIsPersistentToRefreshToken.Designer.cs | 252 + ...210174020_AddIsPersistentToRefreshToken.cs | 29 + ...5755_IncreaseConfigColumnsSize.Designer.cs | 252 + ...0251211135755_IncreaseConfigColumnsSize.cs | 106 + .../ApplicationDbContextModelSnapshot.cs | 249 + .../Models/Configuracion.cs | 138 + .../Models/DTOs/ConfiguracionDto.cs | 95 + Backend/GestorFacturas.API/Models/Evento.cs | 48 + .../GestorFacturas.API/Models/RefreshToken.cs | 29 + Backend/GestorFacturas.API/Models/Usuario.cs | 17 + Backend/GestorFacturas.API/Program.cs | 108 + .../Properties/launchSettings.json | 23 + .../Services/AuthService.cs | 74 + .../Services/EncryptionService.cs | 78 + .../Services/Interfaces/IEncryptionService.cs | 7 + .../Services/Interfaces/IMailService.cs | 21 + .../Interfaces/IProcesadorFacturasService.cs | 13 + .../Services/MailService.cs | 143 + .../Services/ProcesadorFacturasService.cs | 483 ++ .../Workers/CronogramaWorker.cs | 155 + .../appsettings.Development.json | 8 + Backend/GestorFacturas.API/appsettings.json | 49 + README.md | 158 + docker-compose.yml | 42 + frontend/.env.example | 2 + frontend/.gitignore | 24 + frontend/Dockerfile | 29 + frontend/README.md | 73 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/nginx.conf | 23 + frontend/package-lock.json | 4299 +++++++++++++++++ frontend/package.json | 40 + frontend/postcss.config.js | 6 + frontend/public/folder.png | Bin 0 -> 44763 bytes frontend/src/App.css | 42 + frontend/src/App.tsx | 34 + .../Configuracion/ConfiguracionAccesos.tsx | 239 + .../Configuracion/ConfiguracionAlertas.tsx | 200 + .../Configuracion/ConfiguracionPanel.tsx | 209 + .../Configuracion/ConfiguracionTiempos.tsx | 158 + .../components/Dashboard/ControlServicio.tsx | 124 + .../src/components/Dashboard/Dashboard.tsx | 88 + .../components/Dashboard/EjecucionManual.tsx | 101 + .../Dashboard/EstadisticasCards.tsx | 114 + .../components/Dashboard/ExecutionCard.tsx | 106 + .../src/components/Eventos/TablaEventos.tsx | 223 + frontend/src/components/Layout/Header.tsx | 130 + frontend/src/components/Login.tsx | 123 + frontend/src/context/AuthContext.tsx | 44 + frontend/src/index.css | 85 + frontend/src/main.tsx | 10 + frontend/src/services/api.ts | 129 + frontend/src/types/index.ts | 81 + frontend/src/utils/storage.ts | 44 + frontend/tailwind.config.js | 26 + frontend/tsconfig.app.json | 28 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 17 + 78 files changed, 11451 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Backend/GestorFacturas.API/Controllers/AuthController.cs create mode 100644 Backend/GestorFacturas.API/Controllers/ConfiguracionController.cs create mode 100644 Backend/GestorFacturas.API/Controllers/OperacionesController.cs create mode 100644 Backend/GestorFacturas.API/Data/ApplicationDbContext.cs create mode 100644 Backend/GestorFacturas.API/Data/ApplicationDbContextFactory.cs create mode 100644 Backend/GestorFacturas.API/Dockerfile create mode 100644 Backend/GestorFacturas.API/GestorFacturas.API.csproj create mode 100644 Backend/GestorFacturas.API/GestorFacturas.API.http create mode 100644 Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.Designer.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.Designer.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.Designer.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.Designer.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.Designer.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.Designer.cs create mode 100644 Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.cs create mode 100644 Backend/GestorFacturas.API/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 Backend/GestorFacturas.API/Models/Configuracion.cs create mode 100644 Backend/GestorFacturas.API/Models/DTOs/ConfiguracionDto.cs create mode 100644 Backend/GestorFacturas.API/Models/Evento.cs create mode 100644 Backend/GestorFacturas.API/Models/RefreshToken.cs create mode 100644 Backend/GestorFacturas.API/Models/Usuario.cs create mode 100644 Backend/GestorFacturas.API/Program.cs create mode 100644 Backend/GestorFacturas.API/Properties/launchSettings.json create mode 100644 Backend/GestorFacturas.API/Services/AuthService.cs create mode 100644 Backend/GestorFacturas.API/Services/EncryptionService.cs create mode 100644 Backend/GestorFacturas.API/Services/Interfaces/IEncryptionService.cs create mode 100644 Backend/GestorFacturas.API/Services/Interfaces/IMailService.cs create mode 100644 Backend/GestorFacturas.API/Services/Interfaces/IProcesadorFacturasService.cs create mode 100644 Backend/GestorFacturas.API/Services/MailService.cs create mode 100644 Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs create mode 100644 Backend/GestorFacturas.API/Workers/CronogramaWorker.cs create mode 100644 Backend/GestorFacturas.API/appsettings.Development.json create mode 100644 Backend/GestorFacturas.API/appsettings.json create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/folder.png create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/Configuracion/ConfiguracionAccesos.tsx create mode 100644 frontend/src/components/Configuracion/ConfiguracionAlertas.tsx create mode 100644 frontend/src/components/Configuracion/ConfiguracionPanel.tsx create mode 100644 frontend/src/components/Configuracion/ConfiguracionTiempos.tsx create mode 100644 frontend/src/components/Dashboard/ControlServicio.tsx create mode 100644 frontend/src/components/Dashboard/Dashboard.tsx create mode 100644 frontend/src/components/Dashboard/EjecucionManual.tsx create mode 100644 frontend/src/components/Dashboard/EstadisticasCards.tsx create mode 100644 frontend/src/components/Dashboard/ExecutionCard.tsx create mode 100644 frontend/src/components/Eventos/TablaEventos.tsx create mode 100644 frontend/src/components/Layout/Header.tsx create mode 100644 frontend/src/components/Login.tsx create mode 100644 frontend/src/context/AuthContext.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/utils/storage.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2a39b78 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Copiar este archivo a .env y completar los valores + +# --- SQL SERVER --- +DB_HOST=192.168.x.x,1433 +DB_NAME=AdminFacturasApp +DB_USER=gestorFacturasApi +DB_PASSWORD=cambiar_esto + +# --- JWT --- +JWT_KEY=cambiar_por_clave_larga_min_32_chars +JWT_ISSUER=GestorFacturasAPI +JWT_AUDIENCE=GestorFacturasFrontend + +# --- FRONTEND --- +API_PUBLIC_URL=http://localhost:5000/api \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3b310c --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +# =========================== +# SISTEMA OPERATIVO & IDEs +# =========================== +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio Code / Visual Studio +.vs/ +*.njsproj +*.sln +!*.sln # (Opcional: Si quieres versionar la solución, comenta la línea de arriba y descomenta esta. Generalmente sí se versiona el .sln) + +# =========================== +# BACKEND (.NET / C#) +# =========================== +# Build results +[Bb]in/ +[Oo]bj/ + +# Nuget +[Pp]ackages/ +*.nupkg +*.snupkg + +# User-specific files +*.csproj.user +*.pubxml.user + +# Configuración local / Secretos +# Mantenemos appsettings.json y appsettings.Development.json +# Pero ignoramos configuraciones locales que puedan contener secretos reales +appsettings.Local.json +appsettings.Production.json +secrets.json + +# Logs +*.log + +# =========================== +# FRONTEND (React / Vite / Node) +# =========================== +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Testing coverage +coverage/ + +# Production build +dist/ +build/ +dist-ssr/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Vite +*.local + +# =========================== +# DOCKER & INFRAESTRUCTURA +# =========================== +docker-compose.override.yml +.docker/ + +# Si mapeaste volúmenes de datos SQL localmente dentro del proyecto (ej: ./sql-data) +sql-data/ +mssql_data/ + +# Archivos temporales +tmp/ +temp/ \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Controllers/AuthController.cs b/Backend/GestorFacturas.API/Controllers/AuthController.cs new file mode 100644 index 0000000..ef346ba --- /dev/null +++ b/Backend/GestorFacturas.API/Controllers/AuthController.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using GestorFacturas.API.Data; +using GestorFacturas.API.Services; +using GestorFacturas.API.Models; + +namespace GestorFacturas.API.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class AuthController : ControllerBase +{ + private readonly ApplicationDbContext _context; + private readonly AuthService _authService; + + public AuthController(ApplicationDbContext context, AuthService authService) + { + _context = context; + _authService = authService; + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginDto login) + { + var usuario = await _context.Usuarios.FirstOrDefaultAsync(u => u.Username == login.Username); + + if (usuario == null || !_authService.VerificarPassword(login.Password, usuario.PasswordHash)) + { + return Unauthorized(new { mensaje = "Credenciales incorrectas" }); + } + + // Generar Tokens + var accessToken = _authService.GenerarAccessToken(usuario); + + // Pasamos la elección del usuario (true/false) + var refreshToken = _authService.GenerarRefreshToken(usuario.Id, login.RememberMe); + + _context.RefreshTokens.Add(refreshToken); + await _context.SaveChangesAsync(); + + return Ok(new + { + token = accessToken, + refreshToken = refreshToken.Token, + usuario = usuario.Username + }); + } + + [HttpPost("refresh-token")] + public async Task RefreshToken([FromBody] RefreshTokenRequest request) + { + var oldRefreshToken = await _context.RefreshTokens + .Include(r => r.Usuario) + .FirstOrDefaultAsync(r => r.Token == request.Token); + + if (oldRefreshToken == null || !oldRefreshToken.IsActive) + { + return Unauthorized(new { mensaje = "Token inválido" }); + } + + // Revocar el anterior + oldRefreshToken.Revoked = DateTime.UtcNow; + + // Generamos el nuevo token heredando la persistencia del anterior. + // Si el usuario marcó "Recordarme" hace 20 días, el nuevo token seguirá siendo persistente. + // Si no lo marcó, seguirá siendo de corta duración. + var newRefreshToken = _authService.GenerarRefreshToken(oldRefreshToken.UsuarioId, oldRefreshToken.IsPersistent); + + var newAccessToken = _authService.GenerarAccessToken(oldRefreshToken.Usuario!); + + _context.RefreshTokens.Add(newRefreshToken); + await _context.SaveChangesAsync(); + + return Ok(new + { + token = newAccessToken, + refreshToken = newRefreshToken.Token, + usuario = oldRefreshToken.Usuario!.Username + }); + } + + [HttpPost("revoke")] + public async Task Revoke([FromBody] RefreshTokenRequest request) + { + var token = request.Token; + + // Buscamos el token en la BD + var refreshToken = await _context.RefreshTokens + .FirstOrDefaultAsync(r => r.Token == token); + + if (refreshToken == null) + return NotFound(new { mensaje = "Token no encontrado" }); + + // Lo revocamos inmediatamente + refreshToken.Revoked = DateTime.UtcNow; + + _context.Update(refreshToken); + await _context.SaveChangesAsync(); + + return Ok(new { mensaje = "Sesión cerrada correctamente" }); + } +} + +// DTOs +public class LoginDto +{ + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public bool RememberMe { get; set; } = false; +} + +public class RefreshTokenRequest +{ + public string Token { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Controllers/ConfiguracionController.cs b/Backend/GestorFacturas.API/Controllers/ConfiguracionController.cs new file mode 100644 index 0000000..50c5e7c --- /dev/null +++ b/Backend/GestorFacturas.API/Controllers/ConfiguracionController.cs @@ -0,0 +1,316 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Data.SqlClient; +using GestorFacturas.API.Data; +using GestorFacturas.API.Models; +using GestorFacturas.API.Models.DTOs; +using GestorFacturas.API.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; + +namespace GestorFacturas.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class ConfiguracionController : ControllerBase +{ + private readonly ApplicationDbContext _context; + private readonly IMailService _mailService; + private readonly ILogger _logger; + private readonly IEncryptionService _encryptionService; + + public ConfiguracionController( + ApplicationDbContext context, + IMailService mailService, + ILogger logger, + IEncryptionService encryptionService) + { + _context = context; + _mailService = mailService; + _logger = logger; + _encryptionService = encryptionService; + } + + [HttpGet] + public async Task> ObtenerConfiguracion() + { + try + { + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + + if (config == null) + { + return NotFound(new { mensaje = "No se encontró la configuración del sistema" }); + } + + var dto = MapearADto(config); + + // --- LÓGICA PARA CALCULAR PRÓXIMA EJECUCIÓN --- + if (config.EnEjecucion && config.UltimaEjecucion.HasValue) + { + dto.ProximaEjecucion = CalcularFechaProxima(config); + } + else + { + dto.ProximaEjecucion = null; // Si está detenido o nunca corrió + } + + // DESENCRIPTAR PARA MOSTRAR AL USUARIO + dto.DBUsuario = _encryptionService.Decrypt(config.DBUsuario ?? ""); + dto.DBClave = _encryptionService.Decrypt(config.DBClave ?? ""); + dto.SMTPUsuario = _encryptionService.Decrypt(config.SMTPUsuario ?? ""); + dto.SMTPClave = _encryptionService.Decrypt(config.SMTPClave ?? ""); + + return Ok(dto); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener configuración"); + return StatusCode(500, new { mensaje = "Error al obtener la configuración" }); + } + } + + [HttpPut] + public async Task> ActualizarConfiguracion([FromBody] ConfiguracionDto dto) + { + try + { + if (dto.Periodicidad == "Minutos" && dto.ValorPeriodicidad < 15) + { + return BadRequest(new { mensaje = "La periodicidad mínima permitida es de 15 minutos." }); + } + + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + + if (config == null) + { + return NotFound(new { mensaje = "No se encontró la configuración del sistema" }); + } + + config.Periodicidad = dto.Periodicidad; + config.ValorPeriodicidad = dto.ValorPeriodicidad; + config.HoraEjecucion = dto.HoraEjecucion; + config.EnEjecucion = dto.EnEjecucion; + + config.DBServidor = dto.DBServidor; + config.DBNombre = dto.DBNombre; + config.DBTrusted = dto.DBTrusted; + + config.RutaFacturas = dto.RutaFacturas; + config.RutaDestino = dto.RutaDestino; + + config.SMTPServidor = dto.SMTPServidor; + config.SMTPPuerto = dto.SMTPPuerto; + config.SMTPSSL = dto.SMTPSSL; + config.SMTPDestinatario = dto.SMTPDestinatario; + config.AvisoMail = dto.AvisoMail; + + // ENCRIPTAR ANTES DE GUARDAR + config.DBUsuario = _encryptionService.Encrypt(dto.DBUsuario ?? ""); + config.DBClave = _encryptionService.Encrypt(dto.DBClave ?? ""); + config.SMTPUsuario = _encryptionService.Encrypt(dto.SMTPUsuario ?? ""); + config.SMTPClave = _encryptionService.Encrypt(dto.SMTPClave ?? ""); + + await _context.SaveChangesAsync(); + + _logger.LogInformation("Configuración actualizada correctamente"); + + var evento = new Evento + { + Fecha = DateTime.Now, + Mensaje = "Configuración del sistema actualizada", + Tipo = "Info" + }; + _context.Eventos.Add(evento); + await _context.SaveChangesAsync(); + + // Devolvemos el DTO tal cual vino (ya tiene los textos planos que el usuario ingresó) + return Ok(dto); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar configuración"); + return StatusCode(500, new { mensaje = "Error al actualizar la configuración" }); + } + } + + [HttpPost("probar-conexion-sql")] + public async Task> ProbarConexionSQL([FromBody] ProbarConexionDto dto) + { + try + { + var builder = new SqlConnectionStringBuilder + { + DataSource = dto.Servidor, + InitialCatalog = dto.NombreDB, + IntegratedSecurity = dto.TrustedConnection, + TrustServerCertificate = true, + ConnectTimeout = 10 + }; + + if (!dto.TrustedConnection) + { + // Aquí usamos los datos directos del DTO (texto plano) porque es una prueba en vivo + builder.UserID = dto.Usuario; + builder.Password = dto.Clave; + } + + using var conexion = new SqlConnection(builder.ConnectionString); + await conexion.OpenAsync(); + + _logger.LogInformation("Prueba de conexión SQL exitosa a {servidor}/{database}", + dto.Servidor, dto.NombreDB); + + return Ok(new + { + exito = true, + mensaje = $"Conexión exitosa a {dto.Servidor}\\{dto.NombreDB}" + }); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Fallo en prueba de conexión SQL"); + return Ok(new + { + exito = false, + mensaje = $"Error: {ex.Message}" + }); + } + } + + [HttpPost("probar-smtp")] + public async Task> ProbarSMTP([FromBody] ProbarSMTPDto dto) + { + try + { + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + if (config == null) + { + return NotFound(new { mensaje = "No se encontró la configuración" }); + } + + // Guardar estado original + var smtpOriginal = (config.SMTPServidor, config.SMTPPuerto, config.SMTPUsuario, + config.SMTPClave, config.SMTPSSL, config.AvisoMail); + + try + { + // Configurar temporalmente para la prueba + config.SMTPServidor = dto.Servidor; + config.SMTPPuerto = dto.Puerto; + config.SMTPSSL = dto.SSL; + config.AvisoMail = true; + + // IMPORTANTE: Encriptar también para la prueba, ya que MailService espera datos encriptados + config.SMTPUsuario = _encryptionService.Encrypt(dto.Usuario); + config.SMTPClave = _encryptionService.Encrypt(dto.Clave); + + await _context.SaveChangesAsync(); + + var exito = await _mailService.EnviarCorreoAsync( + dto.Destinatario, + "Prueba de Configuración SMTP - Gestor Facturas", + "

✓ Prueba Exitosa

La configuración SMTP está correcta y funcionando.

", + true); + + if (exito) + { + return Ok(new { exito = true, mensaje = "Correo de prueba enviado correctamente" }); + } + else + { + return Ok(new { exito = false, mensaje = "No se pudo enviar el correo de prueba" }); + } + } + finally + { + // Restaurar configuración original + config.SMTPServidor = smtpOriginal.SMTPServidor; + config.SMTPPuerto = smtpOriginal.SMTPPuerto; + config.SMTPUsuario = smtpOriginal.SMTPUsuario; + config.SMTPClave = smtpOriginal.SMTPClave; + config.SMTPSSL = smtpOriginal.SMTPSSL; + config.AvisoMail = smtpOriginal.AvisoMail; + await _context.SaveChangesAsync(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en prueba SMTP"); + return Ok(new { exito = false, mensaje = $"Error: {ex.Message}" }); + } + } + + private ConfiguracionDto MapearADto(Configuracion config) + { + return new ConfiguracionDto + { + Id = config.Id, + Periodicidad = config.Periodicidad, + ValorPeriodicidad = config.ValorPeriodicidad, + HoraEjecucion = config.HoraEjecucion, + UltimaEjecucion = config.UltimaEjecucion, + Estado = config.Estado, + EnEjecucion = config.EnEjecucion, + DBServidor = config.DBServidor, + DBNombre = config.DBNombre, + // Nota: Usuario/Clave se mapean vacíos aquí y se llenan desencriptados en el método GET + DBTrusted = config.DBTrusted, + RutaFacturas = config.RutaFacturas, + RutaDestino = config.RutaDestino, + SMTPServidor = config.SMTPServidor, + SMTPPuerto = config.SMTPPuerto, + SMTPSSL = config.SMTPSSL, + SMTPDestinatario = config.SMTPDestinatario, + AvisoMail = config.AvisoMail + }; + } + + private DateTime CalcularFechaProxima(Configuracion config) + { + // Aunque el método que lo llama ya valida, el compilador necesita seguridad dentro de este bloque. + if (!config.UltimaEjecucion.HasValue) + { + return DateTime.Now; + } + + var ultima = config.UltimaEjecucion.Value; + DateTime proxima = ultima; + + // Parsear hora configurada + TimeSpan horaConfig; + if (!TimeSpan.TryParse(config.HoraEjecucion, out horaConfig)) + { + horaConfig = TimeSpan.Zero; + } + + switch (config.Periodicidad.ToUpper()) + { + case "MINUTOS": + proxima = ultima.AddMinutes(config.ValorPeriodicidad); + break; + case "DIAS": + case "DÍAS": + // Sumar días + proxima = ultima.AddDays(config.ValorPeriodicidad); + // Ajustar a la hora específica + proxima = new DateTime(proxima.Year, proxima.Month, proxima.Day, horaConfig.Hours, horaConfig.Minutes, 0); + + // Si al ajustar la hora, la fecha quedó en el pasado (ej: corrió tarde hoy), sumar un día más si es periodicidad diaria + if (proxima < DateTime.Now && config.ValorPeriodicidad == 1) + { + proxima = proxima.AddDays(1); + } + break; + case "MESES": + proxima = ultima.AddMonths(config.ValorPeriodicidad); + proxima = new DateTime(proxima.Year, proxima.Month, proxima.Day, horaConfig.Hours, horaConfig.Minutes, 0); + break; + } + + // Si por alguna razón la próxima calculada es menor a ahora (retraso), la próxima es YA. + if (proxima < DateTime.Now) return DateTime.Now; + + return proxima; + } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Controllers/OperacionesController.cs b/Backend/GestorFacturas.API/Controllers/OperacionesController.cs new file mode 100644 index 0000000..662f323 --- /dev/null +++ b/Backend/GestorFacturas.API/Controllers/OperacionesController.cs @@ -0,0 +1,224 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using GestorFacturas.API.Data; +using GestorFacturas.API.Models; +using GestorFacturas.API.Models.DTOs; +using GestorFacturas.API.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; + +namespace GestorFacturas.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class OperacionesController : ControllerBase +{ + private readonly ApplicationDbContext _context; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public OperacionesController( + ApplicationDbContext context, + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _context = context; + _scopeFactory = scopeFactory; + _logger = logger; + } + + /// + /// Ejecuta el proceso de facturas manualmente sin esperar al cronograma + /// + [HttpPost("ejecutar-manual")] + public async Task> EjecutarManual([FromBody] EjecucionManualDto dto) + { + try + { + // Usamos _context acá solo para validar config rápida (esto es seguro porque es antes del OK) + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + if (config == null) + { + return NotFound(new { mensaje = "No se encontró la configuración del sistema" }); + } + + _logger.LogInformation("Iniciando ejecución manual desde {fecha}", dto.FechaDesde); + + // Registrar evento inicial + var evento = new Evento + { + Fecha = DateTime.Now, + Mensaje = $"Ejecución manual solicitada desde {dto.FechaDesde:dd/MM/yyyy}", + Tipo = "Info" + }; + _context.Eventos.Add(evento); + await _context.SaveChangesAsync(); + + // Ejecutar el proceso en segundo plano (Fire-and-Forget SEGURO) + _ = Task.Run(async () => + { + // CRÍTICO: Creamos un nuevo Scope para que el DbContext viva + // independientemente de la petición HTTP original. + using var scope = _scopeFactory.CreateScope(); + + try + { + // Obtenemos el servicio DESDE el nuevo scope + var procesadorService = scope.ServiceProvider.GetRequiredService(); + + await procesadorService.EjecutarProcesoAsync(dto.FechaDesde); + } + catch (Exception ex) + { + // Es importante loguear acá porque este hilo no tiene contexto HTTP + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "Error crítico durante ejecución manual en background"); + } + }); + + return Ok(new + { + mensaje = "Proceso iniciado correctamente", + fechaDesde = dto.FechaDesde + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al iniciar ejecución manual"); + return StatusCode(500, new { mensaje = "Error al iniciar el proceso" }); + } + } + + /// + /// Obtiene los eventos/logs del sistema con paginación + /// + [HttpGet("logs")] + public async Task>> ObtenerLogs( + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? tipo = null) + { + try + { + if (pageNumber < 1) pageNumber = 1; + if (pageSize < 1 || pageSize > 100) pageSize = 20; + + var query = _context.Eventos.AsQueryable(); + + // Filtrar por tipo si se especifica + if (!string.IsNullOrEmpty(tipo)) + { + query = query.Where(e => e.Tipo == tipo); + } + + // Ordenar por fecha descendente (más recientes primero) + query = query.OrderByDescending(e => e.Fecha); + + var totalCount = await query.CountAsync(); + + var eventos = await query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(e => new EventoDto + { + Id = e.Id, + Fecha = e.Fecha, + Mensaje = e.Mensaje, + Tipo = e.Tipo, + Enviado = e.Enviado + }) + .ToListAsync(); + + var result = new PagedResult + { + Items = eventos, + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize + }; + + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener logs"); + return StatusCode(500, new { mensaje = "Error al obtener los logs" }); + } + } + + /// + /// Obtiene estadísticas del día actual + /// + [HttpGet("estadisticas")] + public async Task> ObtenerEstadisticas() + { + try + { + var hoy = DateTime.Today; + + var eventosHoy = await _context.Eventos + .Where(e => e.Fecha >= hoy) + .GroupBy(e => e.Tipo) + .Select(g => new { Tipo = g.Key, Cantidad = g.Count() }) + .ToListAsync(); + + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + + return Ok(new + { + ultimaEjecucion = config?.UltimaEjecucion, + estado = config?.Estado ?? false, + enEjecucion = config?.EnEjecucion ?? false, + eventosHoy = new + { + total = eventosHoy.Sum(e => e.Cantidad), + errores = eventosHoy.FirstOrDefault(e => e.Tipo == "Error")?.Cantidad ?? 0, + advertencias = eventosHoy.FirstOrDefault(e => e.Tipo == "Warning")?.Cantidad ?? 0, + info = eventosHoy.FirstOrDefault(e => e.Tipo == "Info")?.Cantidad ?? 0 + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener estadísticas"); + return StatusCode(500, new { mensaje = "Error al obtener estadísticas" }); + } + } + + /// + /// Limpia los logs antiguos (opcional) + /// + [HttpDelete("logs/limpiar")] + public async Task> LimpiarLogsAntiguos([FromQuery] int diasAntiguedad = 30) + { + try + { + var fechaLimite = DateTime.Now.AddDays(-diasAntiguedad); + + var eventosAntiguos = await _context.Eventos + .Where(e => e.Fecha < fechaLimite) + .ToListAsync(); + + if (eventosAntiguos.Count > 0) + { + _context.Eventos.RemoveRange(eventosAntiguos); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Se eliminaron {count} eventos antiguos", eventosAntiguos.Count); + + return Ok(new + { + mensaje = $"Se eliminaron {eventosAntiguos.Count} eventos", + cantidad = eventosAntiguos.Count + }); + } + + return Ok(new { mensaje = "No hay eventos antiguos para eliminar", cantidad = 0 }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al limpiar logs antiguos"); + return StatusCode(500, new { mensaje = "Error al limpiar los logs" }); + } + } +} diff --git a/Backend/GestorFacturas.API/Data/ApplicationDbContext.cs b/Backend/GestorFacturas.API/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..75413b9 --- /dev/null +++ b/Backend/GestorFacturas.API/Data/ApplicationDbContext.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using GestorFacturas.API.Models; +namespace GestorFacturas.API.Data; + +public class ApplicationDbContext : DbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + public DbSet Configuraciones { get; set; } + public DbSet Eventos { get; set; } + public DbSet Usuarios { get; set; } + public DbSet RefreshTokens { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.ToTable("Configuraciones"); + entity.HasData(new Configuracion + { + Id = 1, + Periodicidad = "Dias", + ValorPeriodicidad = 1, + HoraEjecucion = "00:00:00", + Estado = true, + EnEjecucion = false, + DBServidor = "TECNICA3", + DBNombre = "", + DBTrusted = true, + RutaFacturas = "", + RutaDestino = "", + SMTPPuerto = 587, + SMTPSSL = true, + AvisoMail = false + }); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Eventos"); + entity.HasIndex(e => e.Fecha); + entity.HasIndex(e => e.Tipo); + }); + + // Seed de Usuario Admin + // Pass: admin123 + modelBuilder.Entity().HasData(new Usuario + { + Id = 1, + Username = "admin", + Nombre = "Administrador", + PasswordHash = "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW" // Hash placeholder, se actualiza al loguear si es necesario o usar uno real + }); + + // Configuración de RefreshToken (Opcional si usas convenciones, pero bueno para ser explícito) + modelBuilder.Entity() + .HasMany() + .WithOne(r => r.Usuario) + .HasForeignKey(r => r.UsuarioId) + .OnDelete(DeleteBehavior.Cascade); + } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Data/ApplicationDbContextFactory.cs b/Backend/GestorFacturas.API/Data/ApplicationDbContextFactory.cs new file mode 100644 index 0000000..bf92272 --- /dev/null +++ b/Backend/GestorFacturas.API/Data/ApplicationDbContextFactory.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace GestorFacturas.API.Data; + +public class ApplicationDbContextFactory : IDesignTimeDbContextFactory +{ + public ApplicationDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + optionsBuilder.UseSqlServer("Server=TECNICA3;Database=AdminFacturasApp;User Id=gestorFacturasApi;Password=Diagonal423;TrustServerCertificate=True;"); + + return new ApplicationDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Dockerfile b/Backend/GestorFacturas.API/Dockerfile new file mode 100644 index 0000000..942b97f --- /dev/null +++ b/Backend/GestorFacturas.API/Dockerfile @@ -0,0 +1,33 @@ +# Etapa de compilación +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Copiar csproj y restaurar dependencias +COPY ["GestorFacturas.API.csproj", "./"] +RUN dotnet restore "GestorFacturas.API.csproj" + +# Copiar todo el código y compilar +COPY . . +RUN dotnet publish "GestorFacturas.API.csproj" -c Release -o /app/publish + +# Etapa final (Runtime) +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . + +# Instalar tzdata para tener las definiciones de zona horaria +USER root +RUN apt-get update && \ + apt-get install -y tzdata && \ + rm -rf /var/lib/apt/lists/* + +# Configurar zona horaria +ENV TZ=America/Buenos_Aires +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# ----------------------- + +# Crear directorios +RUN mkdir -p /app/data/origen && mkdir -p /app/data/destino + +EXPOSE 8080 +ENTRYPOINT ["dotnet", "GestorFacturas.API.dll"] \ No newline at end of file diff --git a/Backend/GestorFacturas.API/GestorFacturas.API.csproj b/Backend/GestorFacturas.API/GestorFacturas.API.csproj new file mode 100644 index 0000000..0702bdf --- /dev/null +++ b/Backend/GestorFacturas.API/GestorFacturas.API.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + c46298eb-1188-434a-a5ff-accd7e3e25e9 + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Backend/GestorFacturas.API/GestorFacturas.API.http b/Backend/GestorFacturas.API/GestorFacturas.API.http new file mode 100644 index 0000000..d68ed52 --- /dev/null +++ b/Backend/GestorFacturas.API/GestorFacturas.API.http @@ -0,0 +1,6 @@ +@GestorFacturas.API_HostAddress = http://localhost:5036 + +GET {{GestorFacturas.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.Designer.cs b/Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.Designer.cs new file mode 100644 index 0000000..f8f528b --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.Designer.cs @@ -0,0 +1,172 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251210145015_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.cs b/Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.cs new file mode 100644 index 0000000..f1995f6 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210145015_InitialCreate.cs @@ -0,0 +1,88 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Configuraciones", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Periodicidad = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + ValorPeriodicidad = table.Column(type: "int", nullable: false), + HoraEjecucion = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + UltimaEjecucion = table.Column(type: "datetime2", nullable: true), + Estado = table.Column(type: "bit", nullable: false), + EnEjecucion = table.Column(type: "bit", nullable: false), + DBServidor = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + DBNombre = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + DBUsuario = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + DBClave = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + DBTrusted = table.Column(type: "bit", nullable: false), + RutaFacturas = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + RutaDestino = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + SMTPServidor = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + SMTPPuerto = table.Column(type: "int", nullable: false), + SMTPUsuario = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + SMTPClave = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + SMTPSSL = table.Column(type: "bit", nullable: false), + SMTPDestinatario = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + AvisoMail = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Configuraciones", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Eventos", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Fecha = table.Column(type: "datetime2", nullable: false), + Mensaje = table.Column(type: "nvarchar(max)", nullable: false), + Tipo = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + Enviado = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Eventos", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Configuraciones", + columns: new[] { "Id", "AvisoMail", "DBClave", "DBNombre", "DBServidor", "DBTrusted", "DBUsuario", "EnEjecucion", "Estado", "HoraEjecucion", "Periodicidad", "RutaDestino", "RutaFacturas", "SMTPClave", "SMTPDestinatario", "SMTPPuerto", "SMTPSSL", "SMTPServidor", "SMTPUsuario", "UltimaEjecucion", "ValorPeriodicidad" }, + values: new object[] { 1, false, null, "", "TECNICA3", true, null, false, true, "00:00:00", "Dias", "", "", null, null, 587, true, null, null, null, 1 }); + + migrationBuilder.CreateIndex( + name: "IX_Eventos_Fecha", + table: "Eventos", + column: "Fecha"); + + migrationBuilder.CreateIndex( + name: "IX_Eventos_Tipo", + table: "Eventos", + column: "Tipo"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Configuraciones"); + + migrationBuilder.DropTable( + name: "Eventos"); + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.Designer.cs b/Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.Designer.cs new file mode 100644 index 0000000..8928b50 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.Designer.cs @@ -0,0 +1,207 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251210155728_AgregaAutenticacion")] + partial class AgregaAutenticacion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); + + modelBuilder.Entity("Usuario", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Nombre") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Usuarios"); + + b.HasData( + new + { + Id = 1, + Nombre = "Administrador", + PasswordHash = "$2a$11$Z5.Cg.y.u.e.t.c.h.a.n.g.e.m.e", + Username = "admin" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.cs b/Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.cs new file mode 100644 index 0000000..bdbf466 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210155728_AgregaAutenticacion.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + /// + public partial class AgregaAutenticacion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Usuarios", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Username = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: false), + Nombre = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Usuarios", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Usuarios", + columns: new[] { "Id", "Nombre", "PasswordHash", "Username" }, + values: new object[] { 1, "Administrador", "$2a$11$Z5.Cg.y.u.e.t.c.h.a.n.g.e.m.e", "admin" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Usuarios"); + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.Designer.cs b/Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.Designer.cs new file mode 100644 index 0000000..3e6654f --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.Designer.cs @@ -0,0 +1,207 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251210164013_UpdateAdminPassword")] + partial class UpdateAdminPassword + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Usuario", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Nombre") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Usuarios"); + + b.HasData( + new + { + Id = 1, + Nombre = "Administrador", + PasswordHash = "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW", + Username = "admin" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.cs b/Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.cs new file mode 100644 index 0000000..f58835e --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210164013_UpdateAdminPassword.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + /// + public partial class UpdateAdminPassword : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "Usuarios", + keyColumn: "Id", + keyValue: 1, + column: "PasswordHash", + value: "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "Usuarios", + keyColumn: "Id", + keyValue: 1, + column: "PasswordHash", + value: "$2a$11$Z5.Cg.y.u.e.t.c.h.a.n.g.e.m.e"); + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.Designer.cs b/Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.Designer.cs new file mode 100644 index 0000000..84951ae --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.Designer.cs @@ -0,0 +1,249 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251210171018_AddRefreshTokens")] + partial class AddRefreshTokens + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("Revoked") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UsuarioId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UsuarioId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Usuario", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Nombre") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Usuarios"); + + b.HasData( + new + { + Id = 1, + Nombre = "Administrador", + PasswordHash = "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW", + Username = "admin" + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.HasOne("GestorFacturas.API.Models.Usuario", "Usuario") + .WithMany() + .HasForeignKey("UsuarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Usuario"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.cs b/Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.cs new file mode 100644 index 0000000..46effa1 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210171018_AddRefreshTokens.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + /// + public partial class AddRefreshTokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Token = table.Column(type: "nvarchar(max)", nullable: false), + Expires = table.Column(type: "datetime2", nullable: false), + Created = table.Column(type: "datetime2", nullable: false), + Revoked = table.Column(type: "datetime2", nullable: true), + UsuarioId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_RefreshTokens_Usuarios_UsuarioId", + column: x => x.UsuarioId, + principalTable: "Usuarios", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_UsuarioId", + table: "RefreshTokens", + column: "UsuarioId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshTokens"); + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.Designer.cs b/Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.Designer.cs new file mode 100644 index 0000000..4ca7796 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.Designer.cs @@ -0,0 +1,252 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251210174020_AddIsPersistentToRefreshToken")] + partial class AddIsPersistentToRefreshToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("IsPersistent") + .HasColumnType("bit"); + + b.Property("Revoked") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UsuarioId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UsuarioId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Usuario", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Nombre") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Usuarios"); + + b.HasData( + new + { + Id = 1, + Nombre = "Administrador", + PasswordHash = "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW", + Username = "admin" + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.HasOne("GestorFacturas.API.Models.Usuario", "Usuario") + .WithMany() + .HasForeignKey("UsuarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Usuario"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.cs b/Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.cs new file mode 100644 index 0000000..3060452 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251210174020_AddIsPersistentToRefreshToken.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + /// + public partial class AddIsPersistentToRefreshToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsPersistent", + table: "RefreshTokens", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsPersistent", + table: "RefreshTokens"); + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.Designer.cs b/Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.Designer.cs new file mode 100644 index 0000000..8698759 --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.Designer.cs @@ -0,0 +1,252 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251211135755_IncreaseConfigColumnsSize")] + partial class IncreaseConfigColumnsSize + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("IsPersistent") + .HasColumnType("bit"); + + b.Property("Revoked") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UsuarioId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UsuarioId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Usuario", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Nombre") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Usuarios"); + + b.HasData( + new + { + Id = 1, + Nombre = "Administrador", + PasswordHash = "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW", + Username = "admin" + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.HasOne("GestorFacturas.API.Models.Usuario", "Usuario") + .WithMany() + .HasForeignKey("UsuarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Usuario"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.cs b/Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.cs new file mode 100644 index 0000000..d082e7a --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/20251211135755_IncreaseConfigColumnsSize.cs @@ -0,0 +1,106 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + /// + public partial class IncreaseConfigColumnsSize : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "SMTPUsuario", + table: "Configuraciones", + type: "nvarchar(500)", + maxLength: 500, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(200)", + oldMaxLength: 200, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SMTPClave", + table: "Configuraciones", + type: "nvarchar(500)", + maxLength: 500, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(200)", + oldMaxLength: 200, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DBUsuario", + table: "Configuraciones", + type: "nvarchar(500)", + maxLength: 500, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DBClave", + table: "Configuraciones", + type: "nvarchar(500)", + maxLength: 500, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(200)", + oldMaxLength: 200, + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "SMTPUsuario", + table: "Configuraciones", + type: "nvarchar(200)", + maxLength: 200, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(500)", + oldMaxLength: 500, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SMTPClave", + table: "Configuraciones", + type: "nvarchar(200)", + maxLength: 200, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(500)", + oldMaxLength: 500, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DBUsuario", + table: "Configuraciones", + type: "nvarchar(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(500)", + oldMaxLength: 500, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DBClave", + table: "Configuraciones", + type: "nvarchar(200)", + maxLength: 200, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(500)", + oldMaxLength: 500, + oldNullable: true); + } + } +} diff --git a/Backend/GestorFacturas.API/Migrations/ApplicationDbContextModelSnapshot.cs b/Backend/GestorFacturas.API/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..578364d --- /dev/null +++ b/Backend/GestorFacturas.API/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,249 @@ +// +using System; +using GestorFacturas.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GestorFacturas.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GestorFacturas.API.Models.Configuracion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvisoMail") + .HasColumnType("bit"); + + b.Property("DBClave") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DBNombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("DBServidor") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DBTrusted") + .HasColumnType("bit"); + + b.Property("DBUsuario") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EnEjecucion") + .HasColumnType("bit"); + + b.Property("Estado") + .HasColumnType("bit"); + + b.Property("HoraEjecucion") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Periodicidad") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RutaDestino") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RutaFacturas") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPClave") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SMTPDestinatario") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPPuerto") + .HasColumnType("int"); + + b.Property("SMTPSSL") + .HasColumnType("bit"); + + b.Property("SMTPServidor") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SMTPUsuario") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UltimaEjecucion") + .HasColumnType("datetime2"); + + b.Property("ValorPeriodicidad") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Configuraciones", (string)null); + + b.HasData( + new + { + Id = 1, + AvisoMail = false, + DBNombre = "", + DBServidor = "TECNICA3", + DBTrusted = true, + EnEjecucion = false, + Estado = true, + HoraEjecucion = "00:00:00", + Periodicidad = "Dias", + RutaDestino = "", + RutaFacturas = "", + SMTPPuerto = 587, + SMTPSSL = true, + ValorPeriodicidad = 1 + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Evento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Enviado") + .HasColumnType("bit"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("Mensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Fecha"); + + b.HasIndex("Tipo"); + + b.ToTable("Eventos", (string)null); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Expires") + .HasColumnType("datetime2"); + + b.Property("IsPersistent") + .HasColumnType("bit"); + + b.Property("Revoked") + .HasColumnType("datetime2"); + + b.Property("Token") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UsuarioId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UsuarioId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.Usuario", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Nombre") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Usuarios"); + + b.HasData( + new + { + Id = 1, + Nombre = "Administrador", + PasswordHash = "$2a$11$l5UnZIE8bVWSUhYorVqlW.f0qgvK2zsD8aYDyTRXKjtFwwdiAfAvW", + Username = "admin" + }); + }); + + modelBuilder.Entity("GestorFacturas.API.Models.RefreshToken", b => + { + b.HasOne("GestorFacturas.API.Models.Usuario", "Usuario") + .WithMany() + .HasForeignKey("UsuarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Usuario"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/GestorFacturas.API/Models/Configuracion.cs b/Backend/GestorFacturas.API/Models/Configuracion.cs new file mode 100644 index 0000000..3ec663f --- /dev/null +++ b/Backend/GestorFacturas.API/Models/Configuracion.cs @@ -0,0 +1,138 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace GestorFacturas.API.Models; + +/// +/// Entidad que almacena la configuración completa del sistema. +/// Solo existe un registro (ID=1) para toda la aplicación. +/// +public class Configuracion +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + // ===== CONFIGURACIÓN DE PERIODICIDAD ===== + /// + /// Tipo de periodicidad: "Minutos", "Dias", "Meses" + /// + [Required] + [MaxLength(20)] + public string Periodicidad { get; set; } = "Dias"; + + /// + /// Valor numérico de la periodicidad (ej: 1, 5, 30) + /// + [Required] + public int ValorPeriodicidad { get; set; } = 1; + + /// + /// Hora específica de ejecución (formato HH:mm:ss) + /// + [Required] + [MaxLength(10)] + public string HoraEjecucion { get; set; } = "00:00:00"; + + /// + /// Última vez que se ejecutó el proceso + /// + public DateTime? UltimaEjecucion { get; set; } + + /// + /// Resultado del último proceso (true=Exitoso, false=Con Errores) + /// + public bool Estado { get; set; } = true; + + /// + /// Indica si el servicio está activo o detenido + /// + public bool EnEjecucion { get; set; } = false; + + // ===== CONFIGURACIÓN BASE DE DATOS ERP (EXTERNA) ===== + /// + /// Servidor de SQL Server del ERP + /// + [Required] + [MaxLength(200)] + public string DBServidor { get; set; } = "TECNICA3"; + + /// + /// Nombre de la base de datos del ERP + /// + [Required] + [MaxLength(100)] + public string DBNombre { get; set; } = string.Empty; + + /// + /// Usuario de SQL Server (si no usa autenticación integrada) + /// + [MaxLength(500)] + public string? DBUsuario { get; set; } + + /// + /// Contraseña de SQL Server (si no usa autenticación integrada) + /// + [MaxLength(500)] + public string? DBClave { get; set; } + + /// + /// Usar autenticación integrada de Windows (true) o credenciales SQL (false) + /// + public bool DBTrusted { get; set; } = true; + + // ===== RUTAS DE ARCHIVOS ===== + /// + /// Ruta de red donde se buscan los PDFs originales (Origen) + /// + [Required] + [MaxLength(500)] + public string RutaFacturas { get; set; } = string.Empty; + + /// + /// Ruta de red donde se organizan los PDFs procesados (Destino) + /// + [Required] + [MaxLength(500)] + public string RutaDestino { get; set; } = string.Empty; + + // ===== CONFIGURACIÓN SMTP ===== + /// + /// Servidor SMTP para envío de alertas + /// + [MaxLength(200)] + public string? SMTPServidor { get; set; } + + /// + /// Puerto del servidor SMTP + /// + public int SMTPPuerto { get; set; } = 587; + + /// + /// Usuario para autenticación SMTP + /// + [MaxLength(500)] + public string? SMTPUsuario { get; set; } + + /// + /// Contraseña para autenticación SMTP + /// + [MaxLength(500)] + public string? SMTPClave { get; set; } + + /// + /// Usar SSL/TLS para conexión SMTP + /// + public bool SMTPSSL { get; set; } = true; + + /// + /// Dirección de correo destinatario para alertas + /// + [MaxLength(200)] + public string? SMTPDestinatario { get; set; } + + /// + /// Activar/desactivar envío de alertas por correo + /// + public bool AvisoMail { get; set; } = false; +} diff --git a/Backend/GestorFacturas.API/Models/DTOs/ConfiguracionDto.cs b/Backend/GestorFacturas.API/Models/DTOs/ConfiguracionDto.cs new file mode 100644 index 0000000..8645325 --- /dev/null +++ b/Backend/GestorFacturas.API/Models/DTOs/ConfiguracionDto.cs @@ -0,0 +1,95 @@ +namespace GestorFacturas.API.Models.DTOs; + +/// +/// DTO para transferencia de datos de configuración +/// +public class ConfiguracionDto +{ + public int Id { get; set; } + + // Periodicidad + public string Periodicidad { get; set; } = "Dias"; + public int ValorPeriodicidad { get; set; } = 1; + public string HoraEjecucion { get; set; } = "00:00:00"; + public DateTime? UltimaEjecucion { get; set; } + public DateTime? ProximaEjecucion { get; set; } + public bool Estado { get; set; } + public bool EnEjecucion { get; set; } + + // Base de Datos Externa + public string DBServidor { get; set; } = "127.0.0.1"; + public string DBNombre { get; set; } = string.Empty; + public string? DBUsuario { get; set; } + public string? DBClave { get; set; } + public bool DBTrusted { get; set; } = true; + + // Rutas + public string RutaFacturas { get; set; } = string.Empty; + public string RutaDestino { get; set; } = string.Empty; + + // SMTP + public string? SMTPServidor { get; set; } + public int SMTPPuerto { get; set; } = 587; + public string? SMTPUsuario { get; set; } + public string? SMTPClave { get; set; } + public bool SMTPSSL { get; set; } = true; + public string? SMTPDestinatario { get; set; } + public bool AvisoMail { get; set; } +} + +/// +/// DTO para probar conexión a base de datos externa +/// +public class ProbarConexionDto +{ + public string Servidor { get; set; } = string.Empty; + public string NombreDB { get; set; } = string.Empty; + public string? Usuario { get; set; } + public string? Clave { get; set; } + public bool TrustedConnection { get; set; } = true; +} + +/// +/// DTO para probar configuración SMTP +/// +public class ProbarSMTPDto +{ + public string Servidor { get; set; } = string.Empty; + public int Puerto { get; set; } = 587; + public string Usuario { get; set; } = string.Empty; + public string Clave { get; set; } = string.Empty; + public bool SSL { get; set; } = true; + public string Destinatario { get; set; } = string.Empty; +} + +/// +/// DTO para solicitud de ejecución manual +/// +public class EjecucionManualDto +{ + public DateTime FechaDesde { get; set; } = DateTime.Today; +} + +/// +/// DTO para respuesta de evento +/// +public class EventoDto +{ + public int Id { get; set; } + public DateTime Fecha { get; set; } + public string Mensaje { get; set; } = string.Empty; + public string Tipo { get; set; } = string.Empty; + public bool Enviado { get; set; } +} + +/// +/// DTO para respuesta paginada +/// +public class PagedResult +{ + public List Items { get; set; } = new(); + public int TotalCount { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); +} diff --git a/Backend/GestorFacturas.API/Models/Evento.cs b/Backend/GestorFacturas.API/Models/Evento.cs new file mode 100644 index 0000000..c30b129 --- /dev/null +++ b/Backend/GestorFacturas.API/Models/Evento.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace GestorFacturas.API.Models; + +/// +/// Entidad para registro de eventos y auditoría del sistema +/// +public class Evento +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// Fecha y hora del evento + /// + [Required] + public DateTime Fecha { get; set; } = DateTime.Now; + + /// + /// Mensaje descriptivo del evento + /// + [Required] + public string Mensaje { get; set; } = string.Empty; + + /// + /// Tipo de evento: "Info", "Error", "Warning" + /// + [Required] + [MaxLength(20)] + public string Tipo { get; set; } = "Info"; + + /// + /// Indica si este evento ya fue notificado por correo + /// + public bool Enviado { get; set; } = false; +} + +/// +/// Enum para tipos de eventos +/// +public enum TipoEvento +{ + Info, + Error, + Warning +} diff --git a/Backend/GestorFacturas.API/Models/RefreshToken.cs b/Backend/GestorFacturas.API/Models/RefreshToken.cs new file mode 100644 index 0000000..4d434dd --- /dev/null +++ b/Backend/GestorFacturas.API/Models/RefreshToken.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace GestorFacturas.API.Models; + +public class RefreshToken +{ + [Key] + public int Id { get; set; } + + [Required] + public string Token { get; set; } = string.Empty; + + public DateTime Expires { get; set; } + public DateTime Created { get; set; } = DateTime.UtcNow; + public DateTime? Revoked { get; set; } + + public bool IsPersistent { get; set; } + + public bool IsExpired => DateTime.UtcNow >= Expires; + public bool IsActive => Revoked == null && !IsExpired; + + public int UsuarioId { get; set; } + + [ForeignKey("UsuarioId")] + [JsonIgnore] + public Usuario? Usuario { get; set; } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Models/Usuario.cs b/Backend/GestorFacturas.API/Models/Usuario.cs new file mode 100644 index 0000000..c169bac --- /dev/null +++ b/Backend/GestorFacturas.API/Models/Usuario.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +namespace GestorFacturas.API.Models; + +public class Usuario +{ + [Key] + public int Id { get; set; } + + [Required] + [MaxLength(50)] + public string Username { get; set; } = string.Empty; + + [Required] + public string PasswordHash { get; set; } = string.Empty; + + public string Nombre { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Program.cs b/Backend/GestorFacturas.API/Program.cs new file mode 100644 index 0000000..ea1edf8 --- /dev/null +++ b/Backend/GestorFacturas.API/Program.cs @@ -0,0 +1,108 @@ +using Microsoft.EntityFrameworkCore; +using Serilog; +using FluentValidation; +using GestorFacturas.API.Data; +using GestorFacturas.API.Services; +using GestorFacturas.API.Services.Interfaces; +using GestorFacturas.API.Workers; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); +// ===== CONFIGURAR SERILOG (LIMPIO PARA PRODUCCIÓN) ===== +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) // Lee filtros del appsettings + .WriteTo.Console() // ÚNICA SALIDA: Consola (para que Docker/Grafana lo recojan) + .CreateLogger(); + +builder.Host.UseSerilog(); +// ===== SERVICIOS ===== +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +// Entity Framework +builder.Services.AddDbContext(options => +options.UseSqlServer( +builder.Configuration.GetConnectionString("DefaultConnection"), +sqlOptions => sqlOptions.EnableRetryOnFailure() +) +); +// Auth Service +builder.Services.AddScoped(); +// Servicio de Encriptación +builder.Services.AddScoped(); +// JWT Configuration +var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key missing"); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +.AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) + }; +}); +// FluentValidation +builder.Services.AddValidatorsFromAssemblyContaining(); +// Business Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +// Background Service +builder.Services.AddHostedService(); +// CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowReactApp", policy => + { + policy.WithOrigins("http://localhost:5173", "http://localhost:3000", "http://localhost:80") + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); +var app = builder.Build(); +// Migraciones Automáticas +using (var scope = app.Services.CreateScope()) +{ + try + { + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.MigrateAsync(); + Log.Information("Base de datos migrada correctamente"); + } + catch (Exception ex) + { + Log.Error(ex, "Error al aplicar migraciones de base de datos"); + } +} +// Middleware Pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} +app.UseSerilogRequestLogging(); +app.UseHttpsRedirection(); +app.UseCors("AllowReactApp"); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +try +{ + Log.Information("Aplicación Gestor de Facturas iniciada"); + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "La aplicación terminó inesperadamente"); +} +finally +{ + Log.CloseAndFlush(); +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Properties/launchSettings.json b/Backend/GestorFacturas.API/Properties/launchSettings.json new file mode 100644 index 0000000..245ec9d --- /dev/null +++ b/Backend/GestorFacturas.API/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5036", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7067;http://localhost:5036", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Backend/GestorFacturas.API/Services/AuthService.cs b/Backend/GestorFacturas.API/Services/AuthService.cs new file mode 100644 index 0000000..b7da442 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/AuthService.cs @@ -0,0 +1,74 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using GestorFacturas.API.Models; +using GestorFacturas.API.Data; +using BCrypt.Net; + +namespace GestorFacturas.API.Services; + +public class AuthService +{ + private readonly IConfiguration _config; + private readonly ApplicationDbContext _context; + + public AuthService(IConfiguration config, ApplicationDbContext context) + { + _config = config; + _context = context; + } + + public bool VerificarPassword(string password, string hash) + { + try { return BCrypt.Net.BCrypt.Verify(password, hash); } catch { return false; } + } + + public string GenerarHash(string password) + { + return BCrypt.Net.BCrypt.HashPassword(password); + } + + // Generar JWT (Access Token) - Vida corta (ej. 15 min) + public string GenerarAccessToken(Usuario usuario) + { + var keyStr = _config["Jwt:Key"] ?? throw new InvalidOperationException("Jwt:Key no configurada"); + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keyStr)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, usuario.Id.ToString()), + new Claim(ClaimTypes.Name, usuario.Username) + }; + + var token = new JwtSecurityToken( + _config["Jwt:Issuer"], + _config["Jwt:Audience"], + claims, + expires: DateTime.UtcNow.AddMinutes(15), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + // Generar Refresh Token - Vida larga (ej. 7 días) + public RefreshToken GenerarRefreshToken(int usuarioId, bool isPersistent) + { + // Si es persistente: 30 días. + // Si NO es persistente: 12 horas. + var duracion = isPersistent ? TimeSpan.FromDays(30) : TimeSpan.FromHours(12); + + var refreshToken = new RefreshToken + { + Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)), + Expires = DateTime.UtcNow.Add(duracion), + Created = DateTime.UtcNow, + UsuarioId = usuarioId, + IsPersistent = isPersistent + }; + + return refreshToken; + } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Services/EncryptionService.cs b/Backend/GestorFacturas.API/Services/EncryptionService.cs new file mode 100644 index 0000000..3fa2825 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/EncryptionService.cs @@ -0,0 +1,78 @@ +using System.Security.Cryptography; +using System.Text; +using GestorFacturas.API.Services.Interfaces; + +namespace GestorFacturas.API.Services; + +public class EncryptionService : IEncryptionService +{ + private readonly string _key; + + public EncryptionService(IConfiguration config) + { + // La clave debe venir del .env / appsettings + _key = config["EncryptionKey"] ?? throw new ArgumentNullException("EncryptionKey no configurada"); + + // Ajustar si la clave no tiene el tamaño correcto (AES-256 requiere 32 bytes) + // Aquí hacemos un hash SHA256 de la clave para asegurar que siempre tenga 32 bytes válidos + using var sha256 = SHA256.Create(); + var keyBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(_key)); + _key = Convert.ToBase64String(keyBytes); + } + + public string Encrypt(string plainText) + { + if (string.IsNullOrEmpty(plainText)) return plainText; + + var key = Convert.FromBase64String(_key); + using var aes = Aes.Create(); + aes.Key = key; + aes.GenerateIV(); // Generar vector de inicialización aleatorio + + using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); + using var ms = new MemoryStream(); + + // Escribir el IV al principio del stream (necesario para desencriptar) + ms.Write(aes.IV, 0, aes.IV.Length); + + using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + using (var sw = new StreamWriter(cs)) + { + sw.Write(plainText); + } + + return Convert.ToBase64String(ms.ToArray()); + } + + public string Decrypt(string cipherText) + { + if (string.IsNullOrEmpty(cipherText)) return cipherText; + + try + { + var fullCipher = Convert.FromBase64String(cipherText); + var key = Convert.FromBase64String(_key); + + using var aes = Aes.Create(); + aes.Key = key; + + // Extraer el IV (los primeros 16 bytes) + var iv = new byte[16]; + Array.Copy(fullCipher, 0, iv, 0, iv.Length); + aes.IV = iv; + + using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); + using var ms = new MemoryStream(fullCipher, 16, fullCipher.Length - 16); + using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); + using var sr = new StreamReader(cs); + + return sr.ReadToEnd(); + } + catch + { + // Si falla al desencriptar (ej. porque el dato viejo no estaba encriptado), + // devolvemos el texto original para no romper la app en la migración. + return cipherText; + } + } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Services/Interfaces/IEncryptionService.cs b/Backend/GestorFacturas.API/Services/Interfaces/IEncryptionService.cs new file mode 100644 index 0000000..5bc5852 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/Interfaces/IEncryptionService.cs @@ -0,0 +1,7 @@ +namespace GestorFacturas.API.Services.Interfaces; + +public interface IEncryptionService +{ + string Encrypt(string plainText); + string Decrypt(string cipherText); +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/Services/Interfaces/IMailService.cs b/Backend/GestorFacturas.API/Services/Interfaces/IMailService.cs new file mode 100644 index 0000000..d5b3106 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/Interfaces/IMailService.cs @@ -0,0 +1,21 @@ +namespace GestorFacturas.API.Services.Interfaces; + +/// +/// Interfaz para el servicio de envío de correos electrónicos +/// +public interface IMailService +{ + /// + /// Envía un correo electrónico + /// + /// Dirección del destinatario + /// Asunto del correo + /// Cuerpo del mensaje (puede incluir HTML) + /// Indica si el cuerpo es HTML + Task EnviarCorreoAsync(string destinatario, string asunto, string cuerpo, bool esHTML = true); + + /// + /// Prueba la configuración SMTP + /// + Task ProbarConexionAsync(); +} diff --git a/Backend/GestorFacturas.API/Services/Interfaces/IProcesadorFacturasService.cs b/Backend/GestorFacturas.API/Services/Interfaces/IProcesadorFacturasService.cs new file mode 100644 index 0000000..4e14873 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/Interfaces/IProcesadorFacturasService.cs @@ -0,0 +1,13 @@ +namespace GestorFacturas.API.Services.Interfaces; + +/// +/// Interfaz para el servicio principal de procesamiento de facturas +/// +public interface IProcesadorFacturasService +{ + /// + /// Ejecuta el proceso de búsqueda y organización de facturas + /// + /// Fecha desde la cual buscar facturas + Task EjecutarProcesoAsync(DateTime fechaDesde); +} diff --git a/Backend/GestorFacturas.API/Services/MailService.cs b/Backend/GestorFacturas.API/Services/MailService.cs new file mode 100644 index 0000000..f328911 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/MailService.cs @@ -0,0 +1,143 @@ +using MailKit.Net.Smtp; +using MailKit.Security; +using MimeKit; +using GestorFacturas.API.Data; +using GestorFacturas.API.Services.Interfaces; +using GestorFacturas.API.Models; +using Microsoft.EntityFrameworkCore; + +namespace GestorFacturas.API.Services; + +public class MailService : IMailService +{ + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + private readonly IEncryptionService _encryptionService; + + public MailService(ApplicationDbContext context, ILogger logger, IEncryptionService encryptionService) + { + _context = context; + _logger = logger; + _encryptionService = encryptionService; + } + + public async Task EnviarCorreoAsync(string destinatario, string asunto, string cuerpo, bool esHTML = true) + { + try + { + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + + if (config == null || string.IsNullOrEmpty(config.SMTPServidor)) + { + _logger.LogWarning("No hay configuración SMTP disponible"); + return false; + } + + if (!config.AvisoMail) + { + _logger.LogInformation("Envío de correos desactivado en configuración"); + return false; + } + + // --- CORRECCIÓN AQUÍ --- + // Desencriptamos las credenciales AL PRINCIPIO para usarlas en el 'From' y en el 'Authenticate' + string usuarioSmtp = string.IsNullOrEmpty(config.SMTPUsuario) + ? "sistema@eldia.com" + : _encryptionService.Decrypt(config.SMTPUsuario); + + string claveSmtp = string.IsNullOrEmpty(config.SMTPClave) + ? "" + : _encryptionService.Decrypt(config.SMTPClave); + // ----------------------- + + var mensaje = new MimeMessage(); + // Usamos la variable desencriptada + mensaje.From.Add(new MailboxAddress("Gestor Facturas El Día", usuarioSmtp)); + mensaje.To.Add(MailboxAddress.Parse(destinatario)); + mensaje.Subject = asunto; + + var builder = new BodyBuilder(); + if (esHTML) builder.HtmlBody = cuerpo; + else builder.TextBody = cuerpo; + + mensaje.Body = builder.ToMessageBody(); + + using var cliente = new SmtpClient(); + + // Bypass de certificado SSL para redes internas + cliente.ServerCertificateValidationCallback = (s, c, h, e) => true; + + var secureSocketOptions = config.SMTPSSL + ? SecureSocketOptions.StartTls + : SecureSocketOptions.Auto; + + await cliente.ConnectAsync(config.SMTPServidor, config.SMTPPuerto, secureSocketOptions); + + // Usamos las variables que ya desencriptamos arriba + if (!string.IsNullOrEmpty(usuarioSmtp) && !string.IsNullOrEmpty(claveSmtp)) + { + await cliente.AuthenticateAsync(usuarioSmtp, claveSmtp); + } + + await cliente.SendAsync(mensaje); + await cliente.DisconnectAsync(true); + + _logger.LogInformation("Correo enviado exitosamente a {destinatario}", destinatario); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al enviar correo a {destinatario}", destinatario); + return false; + } + } + + public async Task ProbarConexionAsync() + { + // Este método no se está usando actualmente en el flujo crítico (usamos ProbarSMTP en el controller), + // pero por consistencia deberías aplicar la misma lógica de desencriptación si planeas usarlo. + return true; + } + +/* + public async Task ProbarConexionAsync() + { + try + { + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + + if (config == null || string.IsNullOrEmpty(config.SMTPServidor)) + { + return false; + } + + using var cliente = new SmtpClient(); + + cliente.ServerCertificateValidationCallback = (s, c, h, e) => true; + + var secureSocketOptions = config.SMTPSSL + ? SecureSocketOptions.StartTls + : SecureSocketOptions.Auto; + + await cliente.ConnectAsync(config.SMTPServidor, config.SMTPPuerto, secureSocketOptions); + + if (!string.IsNullOrEmpty(config.SMTPUsuario) && !string.IsNullOrEmpty(config.SMTPClave)) + { + // DESENCRIPTAR AQUÍ + var usuario = _encryptionService.Decrypt(config.SMTPUsuario); + var clave = _encryptionService.Decrypt(config.SMTPClave); + + await cliente.AuthenticateAsync(usuario, clave); + } + + await cliente.DisconnectAsync(true); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al probar conexión SMTP"); + return false; + } + }*/ +} diff --git a/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs b/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs new file mode 100644 index 0000000..38158c2 --- /dev/null +++ b/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs @@ -0,0 +1,483 @@ +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using GestorFacturas.API.Data; +using GestorFacturas.API.Models; +using GestorFacturas.API.Services.Interfaces; +using System.Data; + +namespace GestorFacturas.API.Services; + +/// +/// Servicio principal de procesamiento de facturas. +/// +public class ProcesadorFacturasService : IProcesadorFacturasService +{ + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + private readonly IMailService _mailService; + private readonly IEncryptionService _encryptionService; + private readonly IConfiguration _configuration; + + private const int MAX_REINTENTOS = 10; + private const int DELAY_SEGUNDOS = 60; + + public ProcesadorFacturasService( + ApplicationDbContext context, + ILogger logger, + IMailService mailService, + IEncryptionService encryptionService, + IConfiguration configuration) + { + _context = context; + _logger = logger; + _mailService = mailService; + _encryptionService = encryptionService; + _configuration = configuration; + } + + public async Task EjecutarProcesoAsync(DateTime fechaDesde) + { + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); + if (config == null) + { + await RegistrarEventoAsync("No se encontró configuración del sistema", TipoEvento.Error); + return; + } + + // Actualizamos la fecha de ejecución + config.UltimaEjecucion = DateTime.Now; + await _context.SaveChangesAsync(); + + await RegistrarEventoAsync($"Iniciando proceso de facturas desde {fechaDesde:dd/MM/yyyy}", TipoEvento.Info); + + try + { + var facturas = await ObtenerFacturasDesdeERPAsync(config, fechaDesde); + + if (facturas.Count == 0) + { + await RegistrarEventoAsync($"No se encontraron facturas desde {fechaDesde:dd/MM/yyyy}", TipoEvento.Warning); + config.Estado = true; + await _context.SaveChangesAsync(); + return; + } + + await RegistrarEventoAsync($"Se encontraron {facturas.Count} facturas para procesar", TipoEvento.Info); + + int procesadas = 0; + int errores = 0; + List pendientes = new(); + List detallesErroresParaMail = new(); + + // Lista para rastrear las entidades de Evento y actualizar su flag 'Enviado' luego + List eventosDeErrorParaActualizar = new(); + + // --- 1. Primer intento --- + foreach (var factura in facturas) + { + bool exito = await ProcesarFacturaAsync(factura, config); + if (exito) procesadas++; + else pendientes.Add(factura); + } + + // --- 2. Sistema de Reintentos --- + if (pendientes.Count > 0) + { + await RegistrarEventoAsync($"{pendientes.Count} archivos no encontrados. Iniciando sistema de reintentos...", TipoEvento.Warning); + + for (int intento = 1; intento <= MAX_REINTENTOS && pendientes.Count > 0; intento++) + { + await Task.Delay(TimeSpan.FromSeconds(DELAY_SEGUNDOS)); + + var aunPendientes = new List(); + + foreach (var factura in pendientes) + { + bool exito = await ProcesarFacturaAsync(factura, config); + if (exito) + { + procesadas++; + _logger.LogInformation("Archivo encontrado en reintento {intento}: {archivo}", intento, factura.NombreArchivoOrigen); + } + else + { + aunPendientes.Add(factura); + } + } + pendientes = aunPendientes; + } + + // --- 3. Registro de Errores Finales --- + if (pendientes.Count > 0) + { + errores = pendientes.Count; + foreach (var factura in pendientes) + { + string msgError = $"El archivo NO EXISTE después de {MAX_REINTENTOS} intentos: {factura.NombreArchivoOrigen}"; + + var eventoError = new Evento + { + Fecha = DateTime.Now, + Mensaje = msgError, + Tipo = TipoEvento.Error.ToString(), + Enviado = false + }; + + _context.Eventos.Add(eventoError); + + // Guardamos referencias + eventosDeErrorParaActualizar.Add(eventoError); + detallesErroresParaMail.Add(factura.NombreArchivoOrigen); + } + await _context.SaveChangesAsync(); + } + } + + // --- 4. Actualización de Estado General --- + config.Estado = errores == 0; + await _context.SaveChangesAsync(); + + // --- 5. Limpieza automática --- + try + { + var fechaLimiteBorrado = DateTime.Now.AddMonths(-1); + var eventosViejos = _context.Eventos.Where(e => e.Fecha < fechaLimiteBorrado); + if (eventosViejos.Any()) + { + _context.Eventos.RemoveRange(eventosViejos); + await _context.SaveChangesAsync(); + } + } + catch { } + + // --- 6. Evento Final --- + var mensajeFinal = $"Proceso finalizado. Procesadas: {procesadas}, Errores: {errores}"; + await RegistrarEventoAsync(mensajeFinal, errores > 0 ? TipoEvento.Warning : TipoEvento.Info); + + // --- 7. Envío de Mail Inteligente (Solo 1 vez por archivo) --- + if (errores > 0 && config.AvisoMail && !string.IsNullOrEmpty(config.SMTPDestinatario)) + { + // 1. Buscamos en la historia de la DB si estos archivos ya fueron reportados previamente. + // Buscamos en TODOS los logs disponibles (que suelen ser los últimos 30 días según la limpieza). + // Filtramos por Tipo Error y Enviado=true. + var historialErroresEnviados = await _context.Eventos + .Where(e => e.Tipo == TipoEvento.Error.ToString() && e.Enviado == true) + .Select(e => e.Mensaje) + .ToListAsync(); + + // 2. Filtramos la lista actual: + // Solo queremos los archivos que NO aparezcan en ningún mensaje del historial. + var archivosNuevosParaNotificar = detallesErroresParaMail.Where(archivoFallido => + { + // El mensaje en BD es: "El archivo NO EXISTE...: nombre_archivo.pdf" + // Chequeamos si el nombre del archivo está contenido en algún mensaje viejo. + bool yaFueNotificado = historialErroresEnviados.Any(msgHistorico => msgHistorico.Contains(archivoFallido)); + return !yaFueNotificado; + }).ToList(); + + // 3. Decidir si enviar mail + bool mailEnviado = false; + + if (archivosNuevosParaNotificar.Count > 0) + { + // Si hay archivos NUEVOS, enviamos mail SOLO con esos. + // Nota: Pasamos 'errores' (total técnico) y 'archivosNuevosParaNotificar' (detalle visual) + mailEnviado = await EnviarNotificacionErroresAsync( + config.SMTPDestinatario, + procesadas, + errores, + archivosNuevosParaNotificar + ); + + if (mailEnviado) + { + _logger.LogInformation("Correo de alerta enviado con {count} archivos nuevos.", archivosNuevosParaNotificar.Count); + } + } + else + { + _logger.LogInformation("Se omitió el envío de correo: Los {count} errores ya fueron notificados anteriormente.", errores); + // Simulamos que se "envió" (se gestionó) para marcar los flags en BD + mailEnviado = true; + } + + // 4. Actualizar flag en BD (CRÍTICO) + // Si gestionamos la notificación correctamente (ya sea enviándola o detectando que ya estaba enviada), + // marcamos los eventos actuales como Enviado=true para que pasen al historial y no se vuelvan a procesar. + if (mailEnviado && eventosDeErrorParaActualizar.Count > 0) + { + foreach (var evento in eventosDeErrorParaActualizar) + { + evento.Enviado = true; + } + await _context.SaveChangesAsync(); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error crítico en el proceso de facturas"); + string mensajeCritico = $"ERROR CRÍTICO DEL SISTEMA: {ex.Message}"; + + await RegistrarEventoAsync(mensajeCritico, TipoEvento.Error); + + if (config != null) + { + config.Estado = false; + await _context.SaveChangesAsync(); + + if (config.AvisoMail && !string.IsNullOrEmpty(config.SMTPDestinatario)) + { + var listaErroresCriticos = new List { mensajeCritico }; + await EnviarNotificacionErroresAsync(config.SMTPDestinatario, 0, 1, listaErroresCriticos); + } + } + } + } + + private async Task> ObtenerFacturasDesdeERPAsync(Configuracion config, DateTime fechaDesde) + { + var facturas = new List(); + var connectionString = ConstruirCadenaConexion(config); + + try + { + using var conexion = new SqlConnection(connectionString); + await conexion.OpenAsync(); + + var query = @" + SELECT DISTINCT + NUMERO_FACTURA, + TIPO_FACTURA, + CLIENTE, + SUCURSAL_FACTURA, + DIVISION_FACTURA, + FECHACOMPLETA_FACTURA, + NRO_CAI + FROM VISTA_FACTURACION_ELDIA + WHERE SUCURSAL_FACTURA = '70' + AND NRO_CAI IS NOT NULL + AND NRO_CAI != '' + AND FECHACOMPLETA_FACTURA >= @FechaDesde + AND CONVERT(DATE, FECHACOMPLETA_FACTURA) <= CONVERT(DATE, GETDATE()) + ORDER BY FECHACOMPLETA_FACTURA DESC"; + + using var comando = new SqlCommand(query, conexion); + comando.Parameters.AddWithValue("@FechaDesde", fechaDesde); + + using var reader = await comando.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) + { + var factura = new FacturaParaProcesar + { + NumeroFactura = (reader["NUMERO_FACTURA"].ToString() ?? "").Trim().PadLeft(10, '0'), + TipoFactura = (reader["TIPO_FACTURA"].ToString() ?? "").Trim(), + Cliente = (reader["CLIENTE"].ToString() ?? "").Trim().PadLeft(6, '0'), + Sucursal = (reader["SUCURSAL_FACTURA"].ToString() ?? "").Trim().PadLeft(4, '0'), + CodigoEmpresa = (reader["DIVISION_FACTURA"].ToString() ?? "").Trim().PadLeft(4, '0'), + FechaFactura = Convert.ToDateTime(reader["FECHACOMPLETA_FACTURA"]) + }; + + factura.NombreEmpresa = MapearNombreEmpresa(factura.CodigoEmpresa); + factura.NombreArchivoOrigen = ConstruirNombreArchivoOrigen(factura); + factura.NombreArchivoDestino = ConstruirNombreArchivoDestino(factura); + factura.CarpetaDestino = ConstruirRutaCarpetaDestino(factura); + + facturas.Add(factura); + } + } + catch (Exception ex) + { + await RegistrarEventoAsync($"Error al conectar con el ERP: {ex.Message}", TipoEvento.Error); + throw; + } + + return facturas; + } + + private async Task ProcesarFacturaAsync(FacturaParaProcesar factura, Configuracion config) + { + try + { + string rutaBaseOrigen = config.RutaFacturas; + string rutaBaseDestino = config.RutaDestino; + + string rutaOrigen = Path.Combine(rutaBaseOrigen, factura.NombreArchivoOrigen); + string carpetaDestinoFinal = Path.Combine(rutaBaseDestino, factura.CarpetaDestino); + + if (!File.Exists(rutaOrigen)) return false; + + if (!Directory.Exists(carpetaDestinoFinal)) + { + Directory.CreateDirectory(carpetaDestinoFinal); + } + + string rutaDestinoCompleta = Path.Combine(carpetaDestinoFinal, factura.NombreArchivoDestino); + FileInfo infoOrigen = new FileInfo(rutaOrigen); + FileInfo infoDestino = new FileInfo(rutaDestinoCompleta); + + if (infoDestino.Exists) + { + if (infoDestino.Length == infoOrigen.Length) return true; + } + + File.Copy(rutaOrigen, rutaDestinoCompleta, overwrite: true); + return true; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Fallo al copiar {archivo}", factura.NombreArchivoOrigen); + return false; + } + } + + // --- MÉTODOS DE MAPEO (CONFIGURABLES) --- + + private string MapearNombreEmpresa(string codigoEmpresa) + { + var nombre = _configuration[$"EmpresasMapping:{codigoEmpresa}"]; + return string.IsNullOrEmpty(nombre) ? "DESCONOCIDA" : nombre; + } + + private string AjustarTipoFactura(string tipoOriginal) + { + if (string.IsNullOrEmpty(tipoOriginal)) return tipoOriginal; + + // 1. Buscamos mapeo en appsettings.json + var tipoMapeado = _configuration[$"FacturaTiposMapping:{tipoOriginal}"]; + if (!string.IsNullOrEmpty(tipoMapeado)) + { + return tipoMapeado; + } + + // 2. Fallback Legacy si no está mapeado + return tipoOriginal[^1].ToString(); + } + + // --- CONSTRUCCIÓN DE NOMBRES --- + + private string ConstruirNombreArchivoOrigen(FacturaParaProcesar factura) + { + return $"{factura.Cliente}-{factura.CodigoEmpresa}-{factura.Sucursal}-{factura.TipoFactura}-{factura.NumeroFactura}.pdf"; + } + + private string ConstruirNombreArchivoDestino(FacturaParaProcesar factura) + { + // El archivo final conserva el Tipo ORIGINAL + return $"{factura.Cliente}-{factura.CodigoEmpresa}-{factura.Sucursal}-{factura.TipoFactura}-{factura.NumeroFactura}.pdf"; + } + + private string ConstruirRutaCarpetaDestino(FacturaParaProcesar factura) + { + // La carpeta usa el Tipo AJUSTADO + string tipoAjustado = AjustarTipoFactura(factura.TipoFactura); + string anioMes = factura.FechaFactura.ToString("yyyy-MM"); + + string nombreCarpetaFactura = $"{factura.Cliente}-{factura.CodigoEmpresa}-{factura.Sucursal}-{tipoAjustado}-{factura.NumeroFactura}"; + + return Path.Combine(factura.NombreEmpresa, anioMes, nombreCarpetaFactura); + } + + // --- UTILIDADES --- + + private string ConstruirCadenaConexion(Configuracion config) + { + var builder = new SqlConnectionStringBuilder + { + DataSource = config.DBServidor, + InitialCatalog = config.DBNombre, + IntegratedSecurity = config.DBTrusted, + TrustServerCertificate = true, + ConnectTimeout = 30 + }; + + if (!config.DBTrusted) + { + builder.UserID = _encryptionService.Decrypt(config.DBUsuario ?? ""); + builder.Password = _encryptionService.Decrypt(config.DBClave ?? ""); + } + + return builder.ConnectionString; + } + + private async Task RegistrarEventoAsync(string mensaje, TipoEvento tipo) + { + try + { + var evento = new Evento + { + Fecha = DateTime.Now, + Mensaje = mensaje, + Tipo = tipo.ToString(), + Enviado = false + }; + _context.Eventos.Add(evento); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al registrar evento"); + } + } + + private async Task EnviarNotificacionErroresAsync(string destinatario, int procesadas, int errores, List detalles) + { + try + { + var asunto = errores == 1 && detalles.Count > 0 && detalles[0].StartsWith("ERROR CRÍTICO") + ? "ALERTA CRÍTICA: Fallo del Sistema Gestor de Facturas" + : "Alerta: Errores en Procesamiento de Facturas"; + + string listaArchivosHtml = ""; + if (detalles != null && detalles.Count > 0) + { + listaArchivosHtml = "

Detalle de Errores:

    "; + foreach (var archivo in detalles) + { + listaArchivosHtml += $"
  • {archivo}
  • "; + } + listaArchivosHtml += "
"; + } + + var cuerpo = $@" + + +

{asunto}

+

Fecha de Ejecución: {DateTime.Now:dd/MM/yyyy HH:mm:ss}

+
+

Facturas procesadas exitosamente: {procesadas}

+

Facturas con error: {errores}

+
+
{listaArchivosHtml}
+
+

Sistema Gestor de Facturas El Día.

+ + "; + + return await _mailService.EnviarCorreoAsync(destinatario, asunto, cuerpo, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al preparar notificación de errores"); + return false; + } + } +} + +/// +/// Clase auxiliar para representar una factura a procesar +/// +public class FacturaParaProcesar +{ + public string NumeroFactura { get; set; } = string.Empty; + public string TipoFactura { get; set; } = string.Empty; + public string Cliente { get; set; } = string.Empty; + public string Sucursal { get; set; } = string.Empty; + public string CodigoEmpresa { get; set; } = string.Empty; + public string NombreEmpresa { get; set; } = string.Empty; + public DateTime FechaFactura { get; set; } + public string NombreArchivoOrigen { get; set; } = string.Empty; + public string NombreArchivoDestino { get; set; } = string.Empty; + public string CarpetaDestino { get; set; } = string.Empty; +} diff --git a/Backend/GestorFacturas.API/Workers/CronogramaWorker.cs b/Backend/GestorFacturas.API/Workers/CronogramaWorker.cs new file mode 100644 index 0000000..74e53dc --- /dev/null +++ b/Backend/GestorFacturas.API/Workers/CronogramaWorker.cs @@ -0,0 +1,155 @@ +using Microsoft.EntityFrameworkCore; +using GestorFacturas.API.Data; +using GestorFacturas.API.Models; +using GestorFacturas.API.Services.Interfaces; + +namespace GestorFacturas.API.Workers; + +/// +/// Worker Service que ejecuta el procesamiento de facturas de forma programada +/// +public class CronogramaWorker : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + // Eliminamos el Timer antiguo + + public CronogramaWorker( + IServiceProvider serviceProvider, + ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("CronogramaWorker iniciado correctamente."); + + // PeriodicTimer espera a que termine la ejecución antes de contar el siguiente intervalo. + // Iniciamos con un tick de 1 minuto para chequear. + using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); + + try + { + // Bucle infinito mientras el servicio esté activo + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + await VerificarYEjecutar(stoppingToken); + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("CronogramaWorker detenido."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fatal en el ciclo del CronogramaWorker"); + } + } + + private async Task VerificarYEjecutar(CancellationToken stoppingToken) + { + try + { + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var config = await context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1, stoppingToken); + + if (config == null) + { + _logger.LogWarning("No se encontró configuración del sistema"); + return; + } + + // Verificar si el servicio está activo + if (!config.EnEjecucion) + { + // Solo loguear en nivel Debug para no saturar los logs + _logger.LogDebug("Servicio en estado detenido"); + return; + } + + // Determinar si toca ejecutar según la periodicidad + if (!DebeEjecutar(config)) + { + return; + } + + _logger.LogInformation("¡Es momento de ejecutar el proceso de facturas!"); + + // Ejecutar el proceso + var procesador = scope.ServiceProvider.GetRequiredService(); + + // Calcular fecha desde basándonos en la última ejecución o periodicidad + DateTime fechaDesde = CalcularFechaDesde(config); + + await procesador.EjecutarProcesoAsync(fechaDesde); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en CronogramaWorker al verificar y ejecutar"); + } + } + + private bool DebeEjecutar(Configuracion config) + { + if (config.UltimaEjecucion == null) return true; + + var ahora = DateTime.Now; + var ultimaEjecucion = config.UltimaEjecucion.Value; + + if (!TimeSpan.TryParse(config.HoraEjecucion, out TimeSpan horaConfigurada)) + { + horaConfigurada = TimeSpan.Zero; + } + + switch (config.Periodicidad.ToUpper()) + { + case "MINUTOS": + var minutosTranscurridos = (ahora - ultimaEjecucion).TotalMinutes; + return minutosTranscurridos >= config.ValorPeriodicidad; + + case "DIAS": + case "DÍAS": + var diasTranscurridos = (ahora.Date - ultimaEjecucion.Date).Days; + if (diasTranscurridos < config.ValorPeriodicidad) return false; + + var horaActual = ahora.TimeOfDay; + var yaEjecutadoHoy = ultimaEjecucion.Date == ahora.Date; + + // Ejecuta si pasó la hora configurada Y no se ha ejecutado hoy + return horaActual >= horaConfigurada && !yaEjecutadoHoy; + + case "MESES": + var mesesTranscurridos = ((ahora.Year - ultimaEjecucion.Year) * 12) + ahora.Month - ultimaEjecucion.Month; + if (mesesTranscurridos < config.ValorPeriodicidad) return false; + + return ahora.TimeOfDay >= horaConfigurada && !(ultimaEjecucion.Date == ahora.Date); + + default: + return false; + } + } + + private DateTime CalcularFechaDesde(Configuracion config) + { + // Buffer de seguridad de 10 días + int diasBuffer = 10; + + if (config.UltimaEjecucion != null) + { + return config.UltimaEjecucion.Value.Date.AddDays(-diasBuffer); + } + + // Si es la primera vez, usa la periodicidad + buffer + return config.Periodicidad.ToUpper() switch + { + "MINUTOS" => DateTime.Now.AddMinutes(-config.ValorPeriodicidad).AddDays(-diasBuffer), + "DIAS" or "DÍAS" => DateTime.Now.AddDays(-config.ValorPeriodicidad - diasBuffer), + "MESES" => DateTime.Now.AddMonths(-config.ValorPeriodicidad).AddDays(-diasBuffer), + _ => DateTime.Today.AddDays(-diasBuffer) + }; + } +} \ No newline at end of file diff --git a/Backend/GestorFacturas.API/appsettings.Development.json b/Backend/GestorFacturas.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Backend/GestorFacturas.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Backend/GestorFacturas.API/appsettings.json b/Backend/GestorFacturas.API/appsettings.json new file mode 100644 index 0000000..c3ff5db --- /dev/null +++ b/Backend/GestorFacturas.API/appsettings.json @@ -0,0 +1,49 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=TU_SERVIDOR_SQL;Database=AdminFacturasApp;User Id=TU_USUARIO;Password=TU_PASSWORD;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "EmpresasMapping": { + "0001": "ELDIA", + "0002": "PUBLIEXITO", + "0003": "RADIONUEVA", + "0004": "RADIOVIEJA", + "0005": "SIP" + }, + "FacturaTiposMapping": { + "FSR": "A", + "FBR": "B", + "CAI": "NCA", + "CBI": "NCB", + "CAC": "NCA", + "CBC": "NCB", + "DAI": "NDA", + "DBI": "NDB", + "DAC": "NDA", + "DBC": "NDB" + }, + "Jwt": { + "Key": "CLAVE_JWT_MUY_LARGA_AQUI_MINIMO_32_CHARS", + "Issuer": "GestorFacturasAPI", + "Audience": "GestorFacturasFrontend" + }, + "EncryptionKey": "CLAVE_ENCRIPTACION_32_BYTES_AQUI", + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" + } + } + ] + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aedf446 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +--- + +# 📄 Gestor de Facturas - Sistema Automatizado + +![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen) +![Platform](https://img.shields.io/badge/Platform-Docker-blue) +![Backend](https://img.shields.io/badge/.NET-10.0-purple) +![Frontend](https://img.shields.io/badge/React-Vite-cyan) + +Sistema integral para la automatización, organización y monitoreo de archivos de facturación electrónica. Este proyecto es la **migración y modernización** del sistema legacy (VB.NET), reimplementado con una arquitectura orientada a microservicios, contenerización y una interfaz web reactiva. + +--- + +## 🚀 Características Principales + +* **Procesamiento en Segundo Plano (Worker):** Servicio continuo que monitorea la base de datos del ERP y busca los archivos PDF correspondientes en la red. +* **Lógica de Reintentos Inteligente:** Sistema de reintentos ante fallos de I/O y notificaciones por correo con lógica "anti-spam" (evita alertar repetidamente sobre el mismo error). +* **Dashboard en Tiempo Real:** Interfaz web para visualizar el estado del servicio, próxima ejecución, logs de eventos y estadísticas diarias. +* **Configuración Dinámica:** Permite ajustar la periodicidad (minutos, días, meses), rutas y credenciales sin detener el contenedor. +* **Seguridad:** Autenticación mediante JWT, encriptación de credenciales sensibles en base de datos (AES-256) y protección de rutas. +* **Compatibilidad Legacy:** Replica exactamente la estructura de carpetas y nomenclatura de archivos del sistema anterior para asegurar la continuidad del negocio. + +--- + +## 🛠️ Stack Tecnológico + +### Backend (`/Backend`) +* **Framework:** .NET 10.0 (C#) +* **Tipo:** Web API + Hosted Service (Worker) +* **Base de Datos:** SQL Server (Entity Framework Core) +* **Logging:** Serilog (Salida a Consola para integración con Grafana/Loki) + +### Frontend (`/frontend`) +* **Framework:** React 24 + TypeScript +* **Build Tool:** Vite +* **Estilos:** Tailwind CSS +* **Estado:** TanStack Query + +### Infraestructura +* **Docker:** Orquestación con Docker Compose. +* **Nginx:** Servidor web y Proxy Inverso para la API. + +--- + +## 📦 Instalación y Despliegue + +### Prerrequisitos +* Docker y Docker Compose instalados en el servidor. +* Acceso a los volúmenes de red donde se alojan las facturas. + +### 1. Clonar el Repositorio +```bash +git clone https://repo.eldiaservicios.com/dmolinari/GestorWebFacturas.git +cd GestorWebFacturas +``` + +### 2. Configuración de Entorno (.env) +Crea un archivo `.env` en la raíz del proyecto basándote en el siguiente ejemplo. + +```ini +# --- BASE DE DATOS SQL SERVER (ERP) --- +DB_HOST=192.168.X.X,1433 +DB_NAME=AdminFacturasApp +DB_USER=usuario_sql +DB_PASSWORD=tu_password_seguro + +# --- SEGURIDAD (JWT & Encriptación) --- +# Generar claves seguras (mínimo 32 caracteres) +JWT_KEY=Clave_Secreta_Para_Firmar_Tokens_JWT_! +JWT_ISSUER=GestorFacturasAPI +JWT_AUDIENCE=GestorFacturasFrontend +ENCRYPTION_KEY=Clave_32_Bytes_Para_AES_DB_Config + +# --- FRONTEND --- +API_PUBLIC_URL=/api +``` + +### 3. Ejecutar con Docker Compose +```bash +docker-compose up -d --build +``` + +El sistema estará disponible en el puerto 80 del servidor (o el configurado en el `docker-compose.yml`). + +--- + +## ⚙️ Configuración del Sistema + +El sistema utiliza dos niveles de configuración: + +### 1. `appsettings.json` (Backend) +Define mapeos estáticos de negocio. +* **EmpresasMapping:** Relaciona códigos de empresa (ej: "0001") con nombres de carpeta (ej: "ELDIA"). +* **FacturaTiposMapping:** Define la traducción de tipos de comprobante (ej: "FSR" -> "A", "CAI" -> "NCA"). + +### 2. Base de Datos (Tabla `Configuraciones`) +Editable desde el **Frontend > Configuración**. Controla: +* Periodicidad de ejecución. +* Rutas de origen y destino (rutas internas del contenedor). +* Credenciales SMTP y de la base de datos externa (se guardan encriptadas). + +--- + +## 📂 Estructura de Directorios y Volúmenes + +Para que el sistema funcione, los volúmenes de Docker deben mapearse correctamente a las rutas de red del servidor host. + +| Ruta en Contenedor | Descripción | Configuración en Panel Web | +| :--- | :--- | :--- | +| `/app/data/origen` | Carpeta donde el ERP deposita los PDFs originales | `/app/data/origen` | +| `/app/data/destino` | Carpeta donde el sistema organiza los PDFs procesados | `/app/data/destino` | + +**Estructura de salida generada:** +```text +/destino + /NOMBRE_EMPRESA + /YYYY-MM (Año-Mes de la factura) + /CLIENTE-EMPRESA-SUCURSAL-TIPO_AJUSTADO-NUMERO (Carpeta) + /CLIENTE-EMPRESA-SUCURSAL-TIPO_ORIGINAL-NUMERO.pdf (Archivo) +``` + +--- + +## 🛡️ Notas de Migración (Legacy vs Moderno) + +Se han respetado las siguientes reglas críticas para mantener compatibilidad: + +1. **Nomenclatura:** + * **Carpeta:** Usa el "Tipo Ajustado" (ej: `NCA` para Notas de Crédito A). + * **Archivo:** Conserva el "Tipo Original" del ERP (ej: `CAI`). +2. **Alertas:** + * El sistema Legacy enviaba correos informativos en cada ejecución. + * El sistema Moderno opera por **Gestión de Excepción**: Solo envía correo si hay errores no reportados previamente (Lógica Anti-Spam / Anti-Fatiga). +3. **Logs:** + * Salida estándar (`stdout`) habilitada para recolección por Grafana/Portainer. + * Registro en base de datos para auditoría interna visible desde el Dashboard. + +--- + +## 👨‍💻 Desarrollo Local + +Para levantar el entorno de desarrollo fuera de Docker: + +**Backend:** +```bash +cd Backend/GestorFacturas.API +dotnet restore +dotnet user-secrets init +# Configurar secretos locales (ver documentación interna) +dotnet run +``` + +**Frontend:** +```bash +cd frontend +npm install +npm run dev +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..892f49a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + # --- BACKEND --- + backend: + build: + context: ./Backend/GestorFacturas.API + dockerfile: Dockerfile + container_name: gestor_facturas_api + restart: always + environment: + - EncryptionKey=${ENCRYPTION_KEY} + + # Construcción dinámica de la cadena de conexión + - ConnectionStrings__DefaultConnection=Server=${DB_HOST};Database=${DB_NAME};User Id=${DB_USER};Password=${DB_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true + + # Variables de JWT + - Jwt__Key=${JWT_KEY} + - Jwt__Issuer=${JWT_ISSUER} + - Jwt__Audience=${JWT_AUDIENCE} + + # Entorno + - ASPNETCORE_ENVIRONMENT=Production + expose: + - "8080" + volumes: + - /mnt/autofs/Facturas:/app/data/origen + - /mnt/autofs/PDFs:/app/data/destino + - ./logs:/app/logs + + # --- FRONTEND --- + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + # Pasamos la variable del .env al proceso de build de Docker + - VITE_API_URL=${API_PUBLIC_URL} + container_name: gestor_facturas_web + restart: always + ports: + - "80:80" + depends_on: + - backend \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..0a25e4b --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Backend API URL +VITE_API_URL=http://localhost:5036/api diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..5696523 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,29 @@ +# Etapa 1: Construcción +FROM node:24-alpine AS build +WORKDIR /app + +# Argumento para la URL de la API (se pasa desde el docker-compose al construir) +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# Etapa 2: Servidor Web Nginx +FROM nginx:alpine +WORKDIR /usr/share/nginx/html + +# Limpiar default +RUN rm -rf ./* + +# Copiar build de React +COPY --from=build /app/dist . + +# Copiar configuración de Nginx +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..bcf09c3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..c436ff9 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + # 1. Configuración para React Router + location / { + try_files $uri $uri/ /index.html; + } + + # 2. PROXY INVERSO + location /api/ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5e3a0ed --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4299 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.2", + "date-fns": "^4.1.0", + "lucide-react": "^0.559.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.10.1", + "sonner": "^2.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.17", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "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.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", + "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", + "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz", + "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.49.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.559.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.559.0.tgz", + "integrity": "sha512-3ymrkBPXWk3U2bwUDg6TdA6hP5iGDMgPEAMLhchEgTQmA+g0Zk24tOtKtXMx35w1PizTmsBC3RhP88QYm+7mHQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", + "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz", + "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==", + "license": "MIT", + "dependencies": { + "react-router": "7.10.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", + "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..22d69f5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.2", + "date-fns": "^4.1.0", + "lucide-react": "^0.559.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.10.1", + "sonner": "^2.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.17", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..1c87846 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/folder.png b/frontend/public/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..552b2c5bd1715bce33e57cd2f4f8927d03879912 GIT binary patch literal 44763 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 TjU5kzfbZ!OoyVnWmS6q{5RG1u literal 0 HcmV?d00001 diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..86168fb --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,34 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Toaster } from 'sonner'; +import { AuthProvider, useAuth } from './context/AuthContext'; +import Login from './components/Login'; +import Dashboard from './components/Dashboard/Dashboard'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + staleTime: 30000, + }, + }, +}); + +// Componente para manejar la lógica de rutas protegidas +function AppContent() { + const { isAuthenticated } = useAuth(); + return isAuthenticated ? : ; +} + +function App() { + return ( + + + + + + + ); +} +export default App; \ No newline at end of file diff --git a/frontend/src/components/Configuracion/ConfiguracionAccesos.tsx b/frontend/src/components/Configuracion/ConfiguracionAccesos.tsx new file mode 100644 index 0000000..12d1937 --- /dev/null +++ b/frontend/src/components/Configuracion/ConfiguracionAccesos.tsx @@ -0,0 +1,239 @@ +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { Database, FolderOpen, Loader2, Info, AlertTriangle } from 'lucide-react'; +import { toast } from 'sonner'; +import { configuracionApi } from '../../services/api'; +import type { Configuracion } from '../../types'; +import { Save } from 'lucide-react'; + +interface ConfiguracionAccesosProps { + configuracion: Configuracion; + onChange: (updates: Partial) => void; + onSave: () => void; // Nuevo prop + isSaving: boolean; // Nuevo prop +} + +export default function ConfiguracionAccesos({ configuracion, onChange, onSave, isSaving }: ConfiguracionAccesosProps) { + const [probandoSQL, setProbandoSQL] = useState(false); + + const probarSQLMutation = useMutation({ + mutationFn: () => + configuracionApi.probarConexionSQL({ + servidor: configuracion.dbServidor, + nombreDB: configuracion.dbNombre, + usuario: configuracion.dbUsuario, + clave: configuracion.dbClave, + trustedConnection: configuracion.dbTrusted, + }), + onSuccess: (data) => { + if (data.exito) { + toast.success('✓ ' + data.mensaje); + } else { + toast.error('✗ ' + data.mensaje); + } + setProbandoSQL(false); + }, + onError: (error) => { + toast.error(`Error: ${error.message}`); + setProbandoSQL(false); + }, + }); + + return ( +

+ {/* ============================================== + SECCIÓN 1: BASE DE DATOS EXTERNA + ============================================== */} +
+
+ +

Conexión a Base de Datos (DB del Tercero)

+
+ +
+
+
+ + onChange({ dbServidor: e.target.value })} + className="input-field" + placeholder="Ej: FEBO o 192.168.1.X" + /> +
+ +
+ + onChange({ dbNombre: e.target.value })} + className="input-field" + placeholder="Nombre de la BD Externa" + /> +
+
+ + {/* Tipo de autenticación */} +
+ + + {configuracion.dbTrusted && ( +
+ +

+ Atención: La autenticación de Windows no suele funcionar en contenedores Docker Linux. + Se recomienda usar Autenticación de SQL Server (Usuario/Clave). +

+
+ )} +
+ + {/* Credenciales SQL (Mostrar si NO es Trusted o para forzar edición) */} +
+
+ + onChange({ dbUsuario: e.target.value })} + className="input-field" + placeholder="Ej: sa" + disabled={configuracion.dbTrusted} + /> +
+ +
+ + onChange({ dbClave: e.target.value })} + className="input-field" + placeholder="••••••••" + disabled={configuracion.dbTrusted} + /> +
+
+ + {/* Botón probar conexión */} +
+ +
+
+
+ + {/* ============================================== + SECCIÓN 2: RUTAS DE ARCHIVOS (DOCKER VOLUMES) + ============================================== */} +
+
+ +

Rutas de Archivos (Volúmenes Docker)

+
+ + {/* Info Box para Docker */} +
+ +
+

Configuración para Docker/Linux:

+

+ No uses rutas de Windows (\\servidor\...). + Usa las rutas internas donde montaste los volúmenes en el contenedor. +

+
+
+ +
+ {/* Ruta Origen */} +
+ +
+ onChange({ rutaFacturas: e.target.value })} + className={`input-field font-mono text-sm pl-9 ${configuracion.rutaFacturas.includes('\\') ? 'border-yellow-400 focus:ring-yellow-400' : '' + }`} + placeholder="/app/data/origen" + /> +
+ +
+
+ {configuracion.rutaFacturas.includes('\\') && ( +

+ + Advertencia: Estás usando barras invertidas (\). Si usas Docker en Linux, usa barras normales (/). +

+ )} +

+ Debe coincidir con el volumen montado en docker-compose.yml +

+
+ + {/* Ruta Destino */} +
+ +
+ onChange({ rutaDestino: e.target.value })} + className="input-field font-mono text-sm pl-9" + placeholder="/app/data/destino" + /> +
+ +
+
+

+ Donde el sistema guardará los archivos procesados +

+
+
+
+ {/* BOTÓN GUARDAR SECCIÓN (Al final de todo) */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Configuracion/ConfiguracionAlertas.tsx b/frontend/src/components/Configuracion/ConfiguracionAlertas.tsx new file mode 100644 index 0000000..82032fb --- /dev/null +++ b/frontend/src/components/Configuracion/ConfiguracionAlertas.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { Mail, Send, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { configuracionApi } from '../../services/api'; +import type { Configuracion } from '../../types'; +import { Save } from 'lucide-react'; + +interface ConfiguracionAlertasProps { + configuracion: Configuracion; + onChange: (updates: Partial) => void; + onSave: () => void; // Nuevo prop + isSaving: boolean; // Nuevo prop +} + +export default function ConfiguracionAlertas({ configuracion, onChange, onSave, isSaving }: ConfiguracionAlertasProps) { + const [probandoSMTP, setProbandoSMTP] = useState(false); + + const probarSMTPMutation = useMutation({ + mutationFn: () => { + if (!configuracion.smtpServidor || !configuracion.smtpUsuario || !configuracion.smtpDestinatario) { + throw new Error('Completa todos los campos SMTP antes de probar'); + } + + return configuracionApi.probarSMTP({ + servidor: configuracion.smtpServidor, + puerto: configuracion.smtpPuerto, + usuario: configuracion.smtpUsuario, + clave: configuracion.smtpClave || '', + ssl: configuracion.smtpSSL, + destinatario: configuracion.smtpDestinatario, + }); + }, + onSuccess: (data) => { + if (data.exito) { + toast.success('✓ ' + data.mensaje); + } else { + toast.error('✗ ' + data.mensaje); + } + setProbandoSMTP(false); + }, + onError: (error) => { + toast.error(`Error: ${error.message}`); + setProbandoSMTP(false); + }, + }); + + return ( +
+
+ +

Configuración de Alertas por Correo

+
+ +

+ Configura el envío de notificaciones por email cuando ocurran errores en el procesamiento. +

+ + {/* Switch para activar/desactivar */} +
+ +
+ + {/* Configuración SMTP */} +
+
+
+ + onChange({ smtpServidor: e.target.value })} + className="input-field" + placeholder="smtp.ejemplo.com" + disabled={!configuracion.avisoMail} + /> +
+ +
+ + onChange({ smtpPuerto: parseInt(e.target.value) || 587 })} + className="input-field" + placeholder="587" + disabled={!configuracion.avisoMail} + /> +
+
+ +
+
+ + onChange({ smtpUsuario: e.target.value })} + className="input-field" + placeholder="usuario@ejemplo.com" + disabled={!configuracion.avisoMail} + /> +
+ +
+ + onChange({ smtpClave: e.target.value })} + className="input-field" + placeholder="••••••••" + disabled={!configuracion.avisoMail} + /> +
+
+ +
+ + onChange({ smtpDestinatario: e.target.value })} + className="input-field" + placeholder="admin@empresa.com" + disabled={!configuracion.avisoMail} + /> +

+ Dirección que recibirá las alertas de errores +

+
+ +
+ +
+ + {/* Botón probar SMTP */} + +
+ {/* BOTÓN GUARDAR SECCIÓN */} +
+ +
+
+ ); +} diff --git a/frontend/src/components/Configuracion/ConfiguracionPanel.tsx b/frontend/src/components/Configuracion/ConfiguracionPanel.tsx new file mode 100644 index 0000000..4a32ee5 --- /dev/null +++ b/frontend/src/components/Configuracion/ConfiguracionPanel.tsx @@ -0,0 +1,209 @@ +import { useState, useEffect } from 'react'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { Loader2, Trash2 } from 'lucide-react'; // Agregado Trash2 +import { toast } from 'sonner'; +import { configuracionApi, operacionesApi } from '../../services/api'; // Agregado operacionesApi +import ConfiguracionTiempos from './ConfiguracionTiempos'; +import ConfiguracionAccesos from './ConfiguracionAccesos'; +import ConfiguracionAlertas from './ConfiguracionAlertas'; +import type { Configuracion } from '../../types'; + +interface ConfiguracionPanelProps { + onGuardar: () => void; +} + +export default function ConfiguracionPanel({ onGuardar }: ConfiguracionPanelProps) { + const [seccionActiva, setSeccionActiva] = useState<'tiempos' | 'accesos' | 'alertas'>('tiempos'); + const [showConfirm, setShowConfirm] = useState(false); + + const { data: configuracion, isLoading, refetch } = useQuery({ + queryKey: ['configuracion'], + queryFn: configuracionApi.obtener, + }); + + const [formData, setFormData] = useState(null); + + // Sincronizar estado local cuando llega la data del servidor + useEffect(() => { + if (configuracion && !formData) { + setFormData(configuracion); + } + }, [configuracion, formData]); + + // Mutación para guardar configuración + const guardarMutation = useMutation({ + mutationFn: (data: Configuracion) => configuracionApi.actualizar(data), + onSuccess: (data) => { + toast.success('✓ Configuración guardada correctamente'); + setFormData(data); + refetch(); + onGuardar(); + }, + onError: (error) => { + toast.error(`Error al guardar: ${error.message}`); + }, + }); + + // --- NUEVO: Mutación para limpiar logs --- + const limpiarLogsMutation = useMutation({ + mutationFn: () => operacionesApi.limpiarLogs(30), // Borra logs de +30 días + onSuccess: (data) => { + toast.success(data.mensaje || 'Logs eliminados correctamente'); + }, + onError: (error) => { + toast.error(`Error al limpiar logs: ${error.message}`); + }, + }); + + // Función genérica que pasaremos a los hijos para guardar + const handleGuardar = () => { + if (!formData) return; + guardarMutation.mutate(formData); + }; + + // --- NUEVO: Función que faltaba --- + const handleLimpiar = () => { + limpiarLogsMutation.mutate(); + }; + + const secciones = [ + { id: 'tiempos' as const, nombre: 'Tiempos y Periodicidad' }, + { id: 'accesos' as const, nombre: 'Accesos y Rutas' }, + { id: 'alertas' as const, nombre: 'Alertas por Correo' }, + ]; + + if (isLoading || !configuracion) { + return ( +
+ + Cargando configuración... +
+ ); + } + + const currentData = formData || configuracion; + const isSaving = guardarMutation.isPending; + const hayCambios = JSON.stringify(formData) !== JSON.stringify(configuracion); + + return ( +
+
+ {/* Header con Título y Botón de Limpieza */} +
+
+

+ Configuración del Sistema +

+ {/* Indicador Global de Cambios */} + {hayCambios && ( + + Cambios sin guardar + + )} +
+ + {/* Botón para abrir el modal de limpieza */} + +
+ + {/* Pestañas */} +
+ +
+ + {/* Contenido */} +
+ {seccionActiva === 'tiempos' && ( + setFormData((prev) => ({ ...(prev || configuracion), ...updates }))} + onSave={handleGuardar} + isSaving={isSaving} + /> + )} + + {seccionActiva === 'accesos' && ( + setFormData((prev) => ({ ...(prev || configuracion), ...updates }))} + onSave={handleGuardar} + isSaving={isSaving} + /> + )} + + {seccionActiva === 'alertas' && ( + setFormData((prev) => ({ ...(prev || configuracion), ...updates }))} + onSave={handleGuardar} + isSaving={isSaving} + /> + )} +
+ + {/* Modal de Confirmación */} + {showConfirm && ( +
+
+
+ +

¿Estás seguro?

+
+ +

+ Esta acción eliminará todos los logs con más de 30 días de antigüedad. +

+ Esta acción no se puede deshacer. +

+ +
+ + +
+
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Configuracion/ConfiguracionTiempos.tsx b/frontend/src/components/Configuracion/ConfiguracionTiempos.tsx new file mode 100644 index 0000000..66e94f9 --- /dev/null +++ b/frontend/src/components/Configuracion/ConfiguracionTiempos.tsx @@ -0,0 +1,158 @@ +import { Clock, Save, Loader2, Info } from 'lucide-react'; +import type { Configuracion } from '../../types'; +import { addMinutes, addDays, addMonths, format } from 'date-fns'; + +interface ConfiguracionTiemposProps { + configuracion: Configuracion; + onChange: (updates: Partial) => void; + onSave: () => void; + isSaving: boolean; +} + +export default function ConfiguracionTiempos({ configuracion, onChange, onSave, isSaving }: ConfiguracionTiemposProps) { + + // Determinamos el valor mínimo según el tipo seleccionado + const minimoPermitido = configuracion.periodicidad === 'Minutos' ? 15 : 1; + + const calcularProyeccion = () => { + if (!configuracion.valorPeriodicidad) return null; + + const ahora = new Date(); + let proxima = ahora; + const tipo = configuracion.periodicidad; + const valor = configuracion.valorPeriodicidad; + + if (tipo === 'Minutos') { + proxima = addMinutes(ahora, valor); + } else if (tipo === 'Dias') { + proxima = addDays(ahora, valor); + } else if (tipo === 'Meses') { + proxima = addMonths(ahora, valor); + } + + return format(proxima, 'dd/MM/yyyy HH:mm:ss'); + }; + + const proximaEstimada = calcularProyeccion(); + + // Manejador inteligente para el cambio de tipo + const handleTipoChange = (nuevoTipo: string) => { + let nuevoValor = configuracion.valorPeriodicidad; + + // Si cambia a Minutos y el valor es menor a 15, lo corregimos a 15 + if (nuevoTipo === 'Minutos' && nuevoValor < 15) { + nuevoValor = 15; + } + // Si cambia a Días/Meses y el valor es 0 o negativo, lo corregimos a 1 + else if (nuevoTipo !== 'Minutos' && nuevoValor < 1) { + nuevoValor = 1; + } + + onChange({ + periodicidad: nuevoTipo, + valorPeriodicidad: nuevoValor + }); + }; + + // Manejador para el cambio de valor numérico + const handleValorChange = (e: React.ChangeEvent) => { + const val = parseInt(e.target.value) || 0; + // Permitimos escribir, la validación estricta ocurre al cambiar tipo o guardar en backend, + // pero aquí respetamos el min del input HTML para las flechas. + onChange({ valorPeriodicidad: val }); + }; + + // Validación para deshabilitar el botón si no cumple la regla + const esValido = !(configuracion.periodicidad === 'Minutos' && configuracion.valorPeriodicidad < 15) && configuracion.valorPeriodicidad > 0; + + return ( +
+
+ +

Configuración de Periodicidad

+
+ +

+ Define cada cuánto tiempo debe ejecutarse el proceso de organización de facturas. +

+ + {/* inputs */} +
+
+ + +
+ +
+ +
+ + {!esValido && ( + + Mínimo {minimoPermitido} + + )} +
+ {configuracion.periodicidad === 'Minutos' && ( +

+ Mínimo permitido: 15 minutos +

+ )} +
+ + {(configuracion.periodicidad === 'Dias' || configuracion.periodicidad === 'Meses') && ( +
+ + onChange({ horaEjecucion: e.target.value })} + className="input-field" + /> +
+ )} +
+ +
+
+
+

+ Proyección: Si guardas esta configuración, + la próxima ejecución estimada sería alrededor del: +

+

+ {proximaEstimada} +

+
+
+
+ + {/* BOTÓN GUARDAR SECCIÓN */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Dashboard/ControlServicio.tsx b/frontend/src/components/Dashboard/ControlServicio.tsx new file mode 100644 index 0000000..4128223 --- /dev/null +++ b/frontend/src/components/Dashboard/ControlServicio.tsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { Play, Square, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { configuracionApi } from '../../services/api'; +import type { Configuracion } from '../../types'; + +interface ControlServicioProps { + configuracion?: Configuracion; + onUpdate: () => void; + onExecute: () => void; +} + +export default function ControlServicio({ configuracion, onUpdate, onExecute }: ControlServicioProps) { + const [isToggling, setIsToggling] = useState(false); + + const toggleMutation = useMutation({ + mutationFn: async (nuevoEstado: boolean) => { + if (!configuracion) throw new Error('No hay configuración'); + + const updated = { ...configuracion, enEjecucion: nuevoEstado }; + return configuracionApi.actualizar(updated); + }, + onSuccess: (_, nuevoEstado) => { + toast.success( + nuevoEstado ? '✓ Servicio iniciado correctamente' : '⏸️ Servicio detenido' + ); + onUpdate(); + onExecute(); + }, + onError: (error) => { + toast.error(`Error al cambiar estado del servicio: ${error.message}`); + }, + onSettled: () => { + setIsToggling(false); + }, + }); + + const handleToggle = () => { + if (!configuracion) { + toast.error('No se ha cargado la configuración'); + return; + } + + // Validar configuración antes de iniciar + if (!configuracion.enEjecucion) { + if (!configuracion.dbNombre || !configuracion.rutaFacturas || !configuracion.rutaDestino) { + toast.error('Debes configurar la base de datos y las rutas antes de iniciar el servicio'); + return; + } + } + + setIsToggling(true); + toggleMutation.mutate(!configuracion.enEjecucion); + }; + + const estaActivo = configuracion?.enEjecucion || false; + + return ( +
+

Control del Servicio

+ +
+ {/* Indicador visual */} +
+ {estaActivo ? ( +
+
+
+ +
+
+ ) : ( + + )} +
+ + {/* Estado textual */} +
+

+ {estaActivo ? 'Servicio en Ejecución' : 'Servicio Detenido'} +

+

+ {estaActivo + ? 'El proceso se ejecutará según la periodicidad configurada' + : 'El servicio está pausado y no procesará facturas'} +

+
+ + {/* Botón de control */} + +
+
+ ); +} diff --git a/frontend/src/components/Dashboard/Dashboard.tsx b/frontend/src/components/Dashboard/Dashboard.tsx new file mode 100644 index 0000000..f57c301 --- /dev/null +++ b/frontend/src/components/Dashboard/Dashboard.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { operacionesApi, configuracionApi } from '../../services/api'; +import Header from '../Layout/Header'; +import ControlServicio from './ControlServicio'; +import EstadisticasCards from './EstadisticasCards'; +import EjecucionManual from './EjecucionManual'; +import TablaEventos from '../Eventos/TablaEventos'; +import ConfiguracionPanel from '../Configuracion/ConfiguracionPanel'; + +// CONFIGURACIÓN CENTRALIZADA DEL TIEMPO DE REFRESCO +const REFRESH_INTERVAL = 5000; // Sugiero bajarlo a 5s para pruebas, luego subirlo a 10s + +export default function Dashboard() { + const [vistaActual, setVistaActual] = useState<'dashboard' | 'configuracion'>('dashboard'); + + // 1. Obtener estadísticas + const { data: estadisticas, refetch: refetchEstadisticas } = useQuery({ + queryKey: ['estadisticas'], + queryFn: operacionesApi.obtenerEstadisticas, + refetchInterval: REFRESH_INTERVAL, + // IMPORTANTE: Esto permite que siga actualizando aunque minimices la ventana + refetchIntervalInBackground: true, + // IMPORTANTE: Anulamos el global de 30s para que siempre acepte datos frescos + staleTime: 0, + }); + + // 2. Obtener configuración + const { data: configuracion, refetch: refetchConfig } = useQuery({ + queryKey: ['configuracion'], + queryFn: configuracionApi.obtener, + refetchInterval: REFRESH_INTERVAL, + refetchIntervalInBackground: true, // Permitir actualización en segundo plano + staleTime: 0, + }); + + return ( +
+
+ +
+ {vistaActual === 'dashboard' ? ( +
+ {/* Estadísticas */} + + + {/* Control del servicio */} +
+ + +
+ + {/* Tabla de eventos */} +
+
+

+ Registro de Eventos +

+ + + + + + En Vivo + +
+ + +
+
+ ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Dashboard/EjecucionManual.tsx b/frontend/src/components/Dashboard/EjecucionManual.tsx new file mode 100644 index 0000000..bae0701 --- /dev/null +++ b/frontend/src/components/Dashboard/EjecucionManual.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { Calendar, PlayCircle, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { operacionesApi } from '../../services/api'; + +interface EjecucionManualProps { + onExecute: () => void; +} + +export default function EjecucionManual({ onExecute }: EjecucionManualProps) { + const [fechaDesde, setFechaDesde] = useState( + new Date().toISOString().split('T')[0] + ); + + const ejecutarMutation = useMutation({ + mutationFn: () => operacionesApi.ejecutarManual({ fechaDesde }), + onSuccess: () => { + toast.success('✓ Proceso iniciado correctamente'); + toast.info('Revisa los eventos para ver el progreso'); + onExecute(); + }, + onError: (error) => { + toast.error(`Error al ejecutar: ${error.message}`); + }, + }); + + const handleEjecutar = () => { + if (!fechaDesde) { + toast.error('Debes seleccionar una fecha'); + return; + } + + ejecutarMutation.mutate(); + }; + + return ( +
+

+ Ejecución Manual +

+ +

+ Ejecuta el proceso inmediatamente sin esperar al cronograma programado. + Selecciona la fecha desde la cual buscar facturas. +

+ +
+ {/* Selector de fecha */} +
+ +
+
+ +
+ setFechaDesde(e.target.value)} + max={new Date().toISOString().split('T')[0]} + className="input-field pl-10" + /> +
+

+ Se procesarán todas las facturas desde esta fecha hasta hoy +

+
+ + {/* Botón de ejecución */} + + + {/* Información adicional */} +
+

+ Nota: El proceso se ejecutará en segundo plano. Los resultados + aparecerán en la tabla de eventos más abajo. +

+
+
+
+ ); +} diff --git a/frontend/src/components/Dashboard/EstadisticasCards.tsx b/frontend/src/components/Dashboard/EstadisticasCards.tsx new file mode 100644 index 0000000..9881706 --- /dev/null +++ b/frontend/src/components/Dashboard/EstadisticasCards.tsx @@ -0,0 +1,114 @@ +import { Activity, AlertCircle, CheckCircle2, Info } from 'lucide-react'; +import type { Estadisticas, Configuracion } from '../../types'; +import ExecutionCard from './ExecutionCard'; + +interface EstadisticasCardsProps { + estadisticas?: Estadisticas; + configuracion?: Configuracion; +} + +export default function EstadisticasCards({ estadisticas, configuracion }: EstadisticasCardsProps) { + // Extraemos los valores para usarlos más fácilmente + const errores = estadisticas?.eventosHoy?.errores || 0; + const advertencias = estadisticas?.eventosHoy?.advertencias || 0; + + const standardCards = [ + { + titulo: 'Estado Último Proceso', + valor: estadisticas?.ultimaEjecucion + ? estadisticas.estado + ? 'Exitoso' + : 'Con Fallas' + : 'Sin datos', + icono: estadisticas?.estado ? CheckCircle2 : AlertCircle, + color: estadisticas?.estado ? 'text-green-600 bg-green-50' : 'text-red-600 bg-red-50', + borde: estadisticas?.estado ? 'border-green-100' : 'border-red-100', + }, + { + titulo: 'Eventos de Hoy', + valor: estadisticas?.eventosHoy?.total?.toString() || '0', + subtext: 'Registros totales', + extraInfo: ( +
+ {errores > 0 && ( + + {errores} errores + + )} + {advertencias > 0 && ( + + {advertencias} advert. + + )} + {errores === 0 && advertencias === 0 && ( + Sin incidentes + )} +
+ ), + icono: Activity, + color: 'text-purple-600 bg-purple-50', + // Lógica de color del borde basada directamente en los valores + borde: errores > 0 ? 'border-red-200' : (advertencias > 0 ? 'border-amber-200' : 'border-purple-100'), + }, + { + titulo: 'Informativos de Hoy', + valor: estadisticas?.eventosHoy?.info?.toString() || '0', + subtext: 'Logs informativos', + icono: Info, + color: 'text-blue-600 bg-blue-50', + borde: 'border-blue-100', + }, + ]; + + return ( +
+ + {/* 1. Tarjeta Timeline */} +
+ +
+ + {/* 2, 3, 4. Tarjetas Estándar */} + {standardCards.map((card, index) => { + const Icono = card.icono; + return ( +
+ {/* Parte Superior: Título e Icono */} +
+ + {card.titulo} + +
+ +
+
+ + {/* Parte Inferior: Valor Grande y Detalles */} +
+

+ {card.valor} +

+ +
+ {card.extraInfo ? ( + card.extraInfo + ) : ( +

+ {card.subtext || '\u00A0'} +

+ )} +
+
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Dashboard/ExecutionCard.tsx b/frontend/src/components/Dashboard/ExecutionCard.tsx new file mode 100644 index 0000000..3b5bcea --- /dev/null +++ b/frontend/src/components/Dashboard/ExecutionCard.tsx @@ -0,0 +1,106 @@ +import { Clock, PauseCircle, Hourglass } from 'lucide-react'; +import { format, formatDistanceToNow, isPast, addSeconds } from 'date-fns'; +import { es } from 'date-fns/locale'; + +interface ExecutionCardProps { + ultimaEjecucion: string | null | undefined; + proximaEjecucion: string | null | undefined; + enEjecucion: boolean; +} + +export default function ExecutionCard({ ultimaEjecucion, proximaEjecucion, enEjecucion }: ExecutionCardProps) { + + const formatDate = (dateString: string) => { + return format(new Date(dateString), "dd/MM, HH:mm", { locale: es }); // Formato más corto + }; + + const formatRelative = (dateString: string) => { + return formatDistanceToNow(new Date(dateString), { addSuffix: true, locale: es }); + }; + + const isOverdue = proximaEjecucion ? isPast(addSeconds(new Date(proximaEjecucion), -10)) : false; + + return ( +
+ {/* Barra de estado */} +
+ +
{/* Padding reducido a p-4 */} + + {/* Encabezado Compacto */} +
{/* Margin reducido */} +
+ + + Sincronización + +
+ + + {enEjecucion ? 'AUTO' : 'MANUAL'} + +
+ +
{/* Espaciado reducido a space-y-4 */} + + {/* ÚLTIMA EJECUCIÓN */} +
+
+ +

Última

+ {ultimaEjecucion ? ( +
+

+ {formatRelative(ultimaEjecucion)} +

+

+ {formatDate(ultimaEjecucion)} +

+
+ ) : ( +

--

+ )} +
+ + {/* PRÓXIMA EJECUCIÓN */} +
+
+ +

Próxima

+ + {enEjecucion && proximaEjecucion ? ( +
+ {isOverdue ? ( +
+

+ En cola... + +

+
+ ) : ( +
+

+ {formatDate(proximaEjecucion)} hs +

+

+ aprox. {formatRelative(proximaEjecucion)} +

+
+ )} +
+ ) : ( +
+ Detenido +
+ )} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Eventos/TablaEventos.tsx b/frontend/src/components/Eventos/TablaEventos.tsx new file mode 100644 index 0000000..503975b --- /dev/null +++ b/frontend/src/components/Eventos/TablaEventos.tsx @@ -0,0 +1,223 @@ +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { AlertCircle, Info, AlertTriangle, ChevronLeft, ChevronRight, RefreshCw, Search, Copy, Check } from 'lucide-react'; +import { format } from 'date-fns'; +import { es } from 'date-fns/locale'; +import { operacionesApi } from '../../services/api'; +import type { Evento } from '../../types'; +import { toast } from 'sonner'; + +// Definimos las props para recibir el intervalo desde el padre +interface TablaEventosProps { + refreshInterval?: number; +} + +export default function TablaEventos({ refreshInterval = 30000 }: TablaEventosProps) { + const [pageNumber, setPageNumber] = useState(1); + const [tipoFiltro, setTipoFiltro] = useState(''); + const [busqueda, setBusqueda] = useState(''); + const pageSize = 15; + + const CopyButton = ({ text }: { text: string }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + toast.success("Mensaje copiado"); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); + }; + + const { data, isLoading, refetch, isFetching } = useQuery({ + queryKey: ['eventos', pageNumber, tipoFiltro], + queryFn: () => operacionesApi.obtenerLogs(pageNumber, pageSize, tipoFiltro || undefined), + + refetchInterval: refreshInterval, + refetchIntervalInBackground: true, // Clave para que no se detenga al cambiar de pestaña + staleTime: 0, // Asegura que React Query sepa que los datos "caducan" inmediatamente + + placeholderData: (previousData) => previousData, + }); + + const getTipoIcon = (tipo: string) => { + switch (tipo) { + case 'Error': return ; + case 'Warning': return ; + default: return ; + } + }; + + const getTipoClass = (tipo: string) => { + switch (tipo) { + case 'Error': return 'bg-red-50 text-red-700 border-red-200'; + case 'Warning': return 'bg-yellow-50 text-yellow-700 border-yellow-200'; + default: return 'bg-blue-50 text-blue-700 border-blue-200'; + } + }; + + const renderMensaje = (msg: string) => { + const parts = msg.split(/(\S+\.pdf)/g); + return ( + + {parts.map((part, i) => + part.endsWith('.pdf') ? ( + + {part} + + ) : part + )} + + ); + }; + + // Filtrado simple en cliente para la búsqueda (si la API no tiene endpoint de búsqueda textual) + // Si la API soporta búsqueda, deberías añadir 'busqueda' al queryKey y a la llamada API. + const itemsFiltrados = data?.items?.filter(item => + item.mensaje.toLowerCase().includes(busqueda.toLowerCase()) + ) ?? []; + + return ( +
+ {/* Filtros y acciones */} +
+
+ + +
+
+
+ +
+ setBusqueda(e.target.value)} + className="input-field pl-9 py-2 text-sm" + /> +
+ + +
+ + {/* Tabla */} +
+ + + + + + + + + + {isLoading ? ( + + + + ) : itemsFiltrados.length > 0 ? ( + itemsFiltrados.map((evento: Evento) => ( + + + + + + )) + ) : ( + + + + )} + +
FechaTipoMensaje
+
+ + Cargando eventos... +
+
+ {format(new Date(evento.fecha), 'dd/MM/yyyy HH:mm:ss', { locale: es })} + + + {getTipoIcon(evento.tipo)} + {evento.tipo} + + +
+ {renderMensaje(evento.mensaje)} +
+ +
+
+
+
+
+ +
+

Sin eventos

+

No hay registros para mostrar.

+
+
+
+ + {/* Paginación */} + {data && data.totalPages > 1 && ( +
+
+ Página {pageNumber} de {data.totalPages} +
+ +
+ + + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Layout/Header.tsx b/frontend/src/components/Layout/Header.tsx new file mode 100644 index 0000000..0fd216c --- /dev/null +++ b/frontend/src/components/Layout/Header.tsx @@ -0,0 +1,130 @@ +import { FileText, Settings, LogOut, User } from 'lucide-react'; +import { useAuth } from '../../context/AuthContext'; +import { authApi } from '../../services/api'; +import { getRefreshToken } from '../../utils/storage'; +import { toast } from 'sonner'; +import type { Estadisticas } from '../../types'; + +interface HeaderProps { + vistaActual: 'dashboard' | 'configuracion'; + setVistaActual: (vista: 'dashboard' | 'configuracion') => void; + estadisticas?: Estadisticas; +} + +export default function Header({ vistaActual, setVistaActual, estadisticas }: HeaderProps) { + const { usuario, logout } = useAuth(); + + const handleLogout = async () => { + try { + // 1. Intentar invalidar en el servidor (Seguridad) + const token = getRefreshToken(); + if (token) { + await authApi.logout(token); + } + } catch (error) { + console.error('Error al notificar cierre de sesión al servidor', error); + // No bloqueamos el logout visual si falla el servidor + } finally { + // 2. Limpiar cliente y redirigir (UX) + logout(); + toast.success('Sesión cerrada correctamente'); + } + }; + + return ( +
+
+
+ + {/* Logo y Título */} +
+
+
+ +
+
+

+ Gestor de Facturas + {/* Indicador visible en móvil (punto simple) */} + +

+
+ + {usuario} +
+
+
+
+ + {/* Estado del servicio y Navegación */} +
+ + {/* Indicador de Estado (Solo visible en desktop) */} +
+ + {/* Tooltip nativo simple */} +
+ {estadisticas?.enEjecucion + ? "El worker está activo buscando facturas nuevas automáticamente." + : "El servicio está detenido. No se procesarán facturas."} +
+ +
+
+ {/* Anillo de pulso animado solo si está activo */} + {estadisticas?.enEjecucion && ( +
+ )} +
+ + + {estadisticas?.enEjecucion ? 'Monitor Activo' : 'Servicio Detenido'} + +
+ + {/* Separador vertical */} +
+ + {/* Botones de acción */} +
+ + + + + {/* Botón Cerrar Sesión */} + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx new file mode 100644 index 0000000..9e7a1b9 --- /dev/null +++ b/frontend/src/components/Login.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { toast } from 'sonner'; +import { Lock, User, Building2, Eye, EyeOff } from 'lucide-react'; +import { useAuth } from '../context/AuthContext'; + +export default function Login() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState(false); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const { login } = useAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api'; + + const { data } = await axios.post(`${API_URL}/auth/login`, { + username, + password, + rememberMe // true o false + }); + + // Pasamos rememberMe al contexto para que decida el storage + login(data.token, data.refreshToken, data.usuario, rememberMe); + + toast.success(rememberMe ? 'Sesión segura por 30 días' : 'Bienvenido'); + } catch (error) { + console.error(error); + toast.error('Credenciales inválidas'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ +
+
+
+

Iniciar Sesión

+

Gestor de Facturas El Día

+
+ +
+
+ +
+
+ +
+ setUsername(e.target.value)} + className="input-field pl-10" + placeholder="admin" + required + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + className="input-field pl-10 pr-10" // Padding derecho extra para el ojo + placeholder="••••••" + required + /> + +
+
+ + {/* CHECKBOX REMEMBER ME */} +
+ setRememberMe(e.target.checked)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded cursor-pointer" + /> + +
+ + +
+ +
+ Versión 1.0.0 © 2025 El Día +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..cde1a27 --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,44 @@ +import { createContext, useContext, useState, type ReactNode } from 'react'; +import { getToken, getUsuario, setAuthData, clearAuthData } from '../utils/storage'; + +interface AuthContextType { + usuario: string | null; + login: (token: string, refreshToken: string, usuario: string, rememberMe: boolean) => void; + logout: () => void; + isAuthenticated: boolean; +} + +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + // Inicializamos leyendo de cualquiera de los dos storages + const [token, setToken] = useState(getToken()); + const [usuario, setUsuario] = useState(getUsuario()); + + const login = (newToken: string, newRefreshToken: string, newUser: string, rememberMe: boolean) => { + // Usamos la utilidad para guardar en el lugar correcto + setAuthData(newToken, newRefreshToken, newUser, rememberMe); + + setToken(newToken); + setUsuario(newUser); + }; + + const logout = () => { + clearAuthData(); + setToken(null); + setUsuario(null); + window.location.href = '/'; + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) throw new Error('useAuth must be used within an AuthProvider'); + return context; +}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..97af54f --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,85 @@ +@import "tailwindcss"; + +@theme { + --color-primary-50: #f0f9ff; + --color-primary-100: #e0f2fe; + --color-primary-200: #bae6fd; + --color-primary-300: #7dd3fc; + --color-primary-400: #38bdf8; + --color-primary-500: #0ea5e9; + --color-primary-600: #0284c7; + --color-primary-700: #0369a1; + --color-primary-800: #075985; + --color-primary-900: #0c4a6e; +} + +:root { + font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light; + color: #1f2937; + /* text-gray-800 - oscuro para fondos claros */ + background-color: #f5f5f5; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + display: flex; + min-width: 320px; + min-height: 100vh; + color: #1f2937; +} + +#root { + width: 100%; +} + +@layer components { + .card { + @apply bg-white rounded-lg shadow-md p-6; + } + + .btn-primary { + @apply bg-primary-600 hover:bg-primary-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200; + } + + .btn-secondary { + @apply bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg transition-colors duration-200; + } + + .input-field { + @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-gray-900 bg-white; + } + + .label-text { + @apply block text-sm font-medium text-gray-700 mb-1; + } +} + +/* Estilos adicionales para inputs, selects y textareas */ +input, +select, +textarea { + color: #111827 !important; + /* text-gray-900 - texto oscuro */ + background-color: white !important; +} + +input::placeholder, +textarea::placeholder { + color: #9ca3af; + /* text-gray-400 - placeholder gris claro */ +} + +/* Asegurar que los options en select sean legibles */ +select option { + color: #111827; + background-color: white; +} \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..7feab23 --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,129 @@ +import axios from 'axios'; +import type { + Configuracion, + Evento, + PagedResult, + Estadisticas, + ProbarConexionDto, + ProbarSMTPDto, + EjecucionManualDto, + ApiResponse, +} from '../types'; +import { clearAuthData, getRefreshToken, getToken, updateTokens } from '../utils/storage'; + +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api'; + +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Interceptor Request +api.interceptors.request.use((config) => { + const token = getToken(); // Usa la utilidad que busca en ambos + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Interceptor Response +api.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + try { + const refreshToken = getRefreshToken(); // Usa la utilidad + + if (!refreshToken) throw new Error('No refresh token'); + + const { data } = await axios.post(`${API_BASE_URL}/auth/refresh-token`, { + token: refreshToken + }); + + // IMPORTANTE: Actualizamos en el storage correspondiente + updateTokens(data.token, data.refreshToken); + + originalRequest.headers.Authorization = `Bearer ${data.token}`; + return api(originalRequest); + + } catch (refreshError) { + console.error('Sesión expirada', refreshError); + clearAuthData(); + window.location.href = '/'; + return Promise.reject(refreshError); + } + } + return Promise.reject(error); + } +); + +// ===== CONFIGURACIÓN ===== +export const configuracionApi = { + obtener: async (): Promise => { + const { data } = await api.get('/configuracion'); + return data; + }, + + actualizar: async (config: Configuracion): Promise => { + const { data } = await api.put('/configuracion', config); + return data; + }, + + probarConexionSQL: async (dto: ProbarConexionDto): Promise => { + const { data } = await api.post('/configuracion/probar-conexion-sql', dto); + return data; + }, + + probarSMTP: async (dto: ProbarSMTPDto): Promise => { + const { data } = await api.post('/configuracion/probar-smtp', dto); + return data; + }, +}; + +// ===== OPERACIONES ===== +export const operacionesApi = { + ejecutarManual: async (dto: EjecucionManualDto): Promise => { + const { data } = await api.post('/operaciones/ejecutar-manual', dto); + return data; + }, + + obtenerLogs: async ( + pageNumber: number = 1, + pageSize: number = 20, + tipo?: string + ): Promise> => { + const params: any = { pageNumber, pageSize }; + if (tipo) params.tipo = tipo; + + const { data } = await api.get>('/operaciones/logs', { params }); + return data; + }, + + obtenerEstadisticas: async (): Promise => { + const { data } = await api.get('/operaciones/estadisticas'); + return data; + }, + + limpiarLogs: async (diasAntiguedad: number = 30): Promise => { + const { data } = await api.delete('/operaciones/logs/limpiar', { + params: { diasAntiguedad }, + }); + return data; + }, +}; + +export const authApi = { + logout: async (refreshToken: string) => { + // No esperamos respuesta, es un "fire and forget" para el usuario + return api.post('/auth/revoke', { token: refreshToken }); + } +}; + +export default api; \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..7b6a4d1 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,81 @@ +// Tipos y interfaces para el sistema de gestión de facturas + +export interface Configuracion { + id: number; + periodicidad: string; + valorPeriodicidad: number; + horaEjecucion: string; + ultimaEjecucion: string | null; + proximaEjecucion?: string | null; + estado: boolean; + enEjecucion: boolean; + dbServidor: string; + dbNombre: string; + dbUsuario: string | null; + dbClave: string | null; + dbTrusted: boolean; + rutaFacturas: string; + rutaDestino: string; + smtpServidor: string | null; + smtpPuerto: number; + smtpUsuario: string | null; + smtpClave: string | null; + smtpSSL: boolean; + smtpDestinatario: string | null; + avisoMail: boolean; +} + +export interface Evento { + id: number; + fecha: string; + mensaje: string; + tipo: 'Info' | 'Error' | 'Warning'; + enviado: boolean; +} + +export interface PagedResult { + items: T[]; + totalCount: number; + pageNumber: number; + pageSize: number; + totalPages: number; +} + +export interface Estadisticas { + ultimaEjecucion: string | null; + estado: boolean; + enEjecucion: boolean; + eventosHoy: { + total: number; + errores: number; + advertencias: number; + info: number; + }; +} + +export interface ProbarConexionDto { + servidor: string; + nombreDB: string; + usuario: string | null; + clave: string | null; + trustedConnection: boolean; +} + +export interface ProbarSMTPDto { + servidor: string; + puerto: number; + usuario: string; + clave: string; + ssl: boolean; + destinatario: string; +} + +export interface EjecucionManualDto { + fechaDesde: string; +} + +export interface ApiResponse { + exito?: boolean; + mensaje?: string; + data?: T; +} diff --git a/frontend/src/utils/storage.ts b/frontend/src/utils/storage.ts new file mode 100644 index 0000000..405556a --- /dev/null +++ b/frontend/src/utils/storage.ts @@ -0,0 +1,44 @@ +// Detecta dónde están guardados los tokens actualmente +export const getStorageType = (): 'local' | 'session' | null => { + if (localStorage.getItem('token')) return 'local'; + if (sessionStorage.getItem('token')) return 'session'; + return null; +}; + +export const getToken = () => { + return localStorage.getItem('token') || sessionStorage.getItem('token'); +}; + +export const getRefreshToken = () => { + return localStorage.getItem('refreshToken') || sessionStorage.getItem('refreshToken'); +}; + +export const getUsuario = () => { + return localStorage.getItem('usuario') || sessionStorage.getItem('usuario'); +}; + +export const setAuthData = (token: string, refreshToken: string, usuario: string, rememberMe: boolean) => { + const storage = rememberMe ? localStorage : sessionStorage; + + // Limpiamos el otro storage por si acaso había residuos + if (rememberMe) sessionStorage.clear(); + else localStorage.clear(); + + storage.setItem('token', token); + storage.setItem('refreshToken', refreshToken); + storage.setItem('usuario', usuario); +}; + +export const updateTokens = (token: string, refreshToken: string) => { + // Detectamos dónde están guardados para actualizarlos en el mismo lugar + const type = getStorageType(); + const storage = type === 'local' ? localStorage : sessionStorage; + + storage.setItem('token', token); + storage.setItem('refreshToken', refreshToken); +}; + +export const clearAuthData = () => { + localStorage.clear(); + sessionStorage.clear(); +}; \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..744256b --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,26 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + }, + }, + }, + }, + plugins: [], +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..a7df9a6 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + // Cuando el frontend pida "/api", Vite lo redirigirá al backend + '/api': { + target: 'http://localhost:5036', + changeOrigin: true, + secure: false, + } + } + } +}) \ No newline at end of file